前端页面巡检-Puppeteer实战

3,482 阅读8分钟

需求背景

需要前端提供方案,来支持H5或Web静态页面的巡检功能

巡检服务主要包含对页面状态的检测(死链检测),页面的截图等功能

image.png 后期可扩展的功能当然不止这些,还可以去加登陆/UI自动化测试等等....只要你想~

方案选型

方案上直接上Headless browser(无头浏览器),这也是目前比较通用、成本最低的一种方案,无头浏览器的框架也不少,对几个比较大的做了调研:

HeadLess Browser支持语言覆盖浏览器内核支持多标签+表单录制脚本文档资源和社区活跃
Puppeteer只支持 JavaScript & TypeScript \ python只支持 Chromium/FirefoxAPI更友好,更直观支持,基于Puppeteer Recorder录制脚本文档比较齐全,国内检索教程也不少(used by 213k)GitHub 78K Star
PlayWrightJavaScript & TypeScript\python\C#\Go\Java支持Chromium/WebKit/FirefoxAPI更友好,更直观支持,基于 playwright codegen 命令录制脚本文档比较齐全,教程也有一些但不多,比较新(used by 12.8k)Github 40K star
Seleniumjava\python\ruby\C#\C++\JavaScript运行在目前所有主流浏览器上通过 switch_to 切换支持,Selenium IDE可以录制脚本官方文档一般,但作为老牌的框架 教程多一些(used by 149k)Github 24K star
Cypress只支持 JavaScript & TypeScript只支持 Chrome/Firefox没有真正支持不支持(可以使用Cypress Studio,但这是一个实验性的功能)官方文档质量、社区活跃度还不错 (used by 476k)Github 39K star

在这几大框架中个人更偏向于 puppetter 和 playwright:

puppetter 和 playwright都比较新一些,两者的API 也很相似;puppetter 由谷歌于2017年发布;playwright 由微软于2020年1月发布第一个公共版本。

Playwright有一个非常重要的功能,是它对浏览器Context的支持。它能够在单个浏览器实例中运行隔离的操作,因此您可以设置多个Context以同时测试多个Web页面。在每个Context中创建页面。页面支持它们自己的单击交互,并且可以并行监视。进入页面后,可以使用CSS或XPath选择器,HTML属性或文本,以不同的方式查找与之交互的内容。

Playwright 支持的浏览器也比较多一些,不过最终我还是选择了 puppetter ,感兴趣的可以自行尝试 Playwright 去做。

选择 puppetter 的一个主要原因也是成熟的社区和文档,有稳定的团队维护,还有就是我们仅需要跑Chrome浏览器就够了。

另外考虑到学习成本及技术栈(NodeJS),还是建议没玩过这种无头浏览器的前端伙伴从 puppetter 开始。

技术实现

技术实现层面,server 端主要采用egg框架来启服务,没接触过的可以查看Egg官方文档 对外暴露出一个 api 来对巡检服务的调用。

Service 中的主要逻辑就是核心了,是运用 puppetter 去开启无头浏览器,所以封装了一个巡检的类PatrolCore,通过调用实例的 start 方法去开始巡检页面。

image.png

下面说下PatrolCore的主要逻辑,第一步主要是开启无头浏览器,这里采用了puppeteer-cluster 这个包,这个包为你封装了一个类似线程池的这么一套机制,可以自己去定义开启的worker数量,这在多页面巡检和爬取的时候非常有用,我这里开启了最大5个worker, 它的内部会按需复用并在出现错误时去重启浏览器和重试,这样为我们节省了很多需要处理的逻辑。

const cluster = await Cluster.launch({
    concurrency: Cluster.CONCURRENCY_CONTEXT,
    maxConcurrency: 5,
    puppeteerOptions: {
      headless: true,
      args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage' ],
      ignoreDefaultArgs: [ '--disable-extensions' ],
      executablePath: '/usr/bin/chromium-browser', // 指定chromium路径
    }, // 传递给puppeteer.launch的对象
    // perBrowserOptions: [], // 传递给每个浏览器的puppeteer.launch的对象
    // retryLimit: 2, // 在将worker标记为失败之前,您希望多长时间重试一次作业
  });

同时puppeteer-cluster这个包还提供了API帮助你去对每个页面添加任务,或者为所有要巡检的页面统一添加任务

await cluster.task(async ({ page, data: url, worker }) => {
    await this.pageTask(page, url, worker);
});
this.urls.forEach(async url => {
    if (urlCheck(url)) {
      cluster.queue(url);
    } else {
      this.appContext.logger.warn(`The url -> ${url} is Illegal URL! Will not crawl !`);
    }
  });

我这里为所有要巡检的页面添加了统一的任务,就是检测页面状态和截图:

await page.setViewport({
    width: cWidth,
    height: cHeight });
  const gotoPageRes = await page.goto(url, { waitUntil: 'networkidle0' });
  this.appContext.logger.info(`Go to page: ${url}; And current worker id is ${worker.id} .`);
  // 页面打开状态
  this.results[url].status = gotoPageRes.status();
  if (gotoPageRes.status() >= 400) {
    this.appContext.logger.error(`${gotoPageRes.url()} error: status is ${gotoPageRes.status()}`);
  }
  // 页面滚动(获取页面懒加载渲染的部分)
  await pageScroll(page, cHeight);
  ......省略n行代码
  await page.screenshot({ path: imgPath, fullPage: true });

在截图的时候要注意,如果是有懒加载的页面,在截图前要模拟页面滚动,模拟滚动到底部后,截图才可以截取完整,以下部分为模拟滚动的代码:

exports.pageScroll = async (page, cHeight) => {
  // 网页加载最大高度
  const max_height_px = 20000;
  // 滚动高度
  const scrollStep = cHeight;
  const height_limit = false;
  let mValues = { scrollEnable: true, height_limit };
  while (mValues.scrollEnable) {
    mValues = await page.evaluate((scrollStep, max_height_px, height_limit) => {
      // 防止网页没有body时,滚动报错
      if (document.scrollingElement) {
        const scrollTop = document.scrollingElement.scrollTop;
        document.scrollingElement.scrollTop = scrollTop + scrollStep;

        if (document.body !== null && document.body.clientHeight > max_height_px) {
          // eslint-disable-next-line no-param-reassign
          height_limit = true;
        } else if (document.scrollingElement.scrollTop + scrollStep > max_height_px) {
          // eslint-disable-next-line no-param-reassign
          height_limit = true;
        }

        let scrollEnableFlag = false;
        if (document.body !== null) {
          scrollEnableFlag = document.body.clientHeight > scrollTop + 1081 && !height_limit;
        } else {
          scrollEnableFlag = document.scrollingElement.scrollTop + scrollStep > scrollTop + 1081 && !height_limit;
        }

        return {
          scrollEnable: scrollEnableFlag,
          height_limit,
          document_scrolling_Element_scrollTop: document.scrollingElement.scrollTop,
        };
      }

    }, scrollStep, max_height_px, height_limit);
    await sleep(800);
  }
};

关于页面巡检的核心逻辑就是这些了,后续可以按自己的需求在从中增加功能,如自动化测试等等~

部署

巡检工具的最终部署是绕不开的一个环节,也是比较容易踩坑的一个环节

node 服务的部署采用 egg 内置的egg-cluster来启动 Master 进程,这里基本按官方文档来就ok

Linux CentOS8下部署

服务器是 CentOS8 的系统,主要是 Puppeteer 依赖的安装和部署(依赖 chromium)。

中文字体

在Linux中部署后,巡检截图中的文字都是乱码的,这是因为没有中文字体的原因,所以需要我们手动安装中文字体。我这里用的方式是从windows中将字体文件copy出来,上传到服务器,然后安装ttmkfdir 工具:

yum -y install ttmkfdir
ttmkfdir -e /usr/share/X11/fonts/encodings/encodings.dir

接着修改字体配置文件 vim /etc/fonts/fonts.conf:

<!-- Font directory list -->

        <dir>/usr/share/fonts</dir>
        <dir>/usr/share/X11/fonts/Type1</dir> <dir>/usr/share/X11/fonts/TTF</dir> <dir>/usr/local/share/fonts</dir><dir>/usr/local/share/fonts/chinese</dir>
        <dir prefix="xdg">fonts</dir>
        <!-- the following element will be removed in the future -->
        <dir>~/.fonts</dir>

最主要的是这部分:

image.png

配置好后执行 fc-cache 命令(扫描字体目录并生成字体缓存); 最后可以通过 fc-list 命令来检查支持的字体; 配置成功后截图出来的网页就不会有中文乱码啦~

Docker 部署

Puppeteer 的服务通过 Docker 部署也算踩了很多坑,这里先说下node服务的部署,在大多数公司我们的服务都是采用 Docker 部署的,我们的也不例外,Docker 部署一般需要把 egg 服务修改为前台启动:

image.png

image.png

DockerFile

踩了很多坑,也查了不少资料,这里说下我的 DockerFile , 已跑通。

  1. 首先是镜像选择,在镜像选择之前你需要了解 alpine/buster/stretch/jessie/bullseye 版本的区别,这里有一篇博客列出来了可以参考:Docker镜像版本区别 这里我选择公司内部的 apline-node 镜像源(你可以替换为公共node镜像,这里用的是node 16 版本):
FROM xxx.xxx.com/alpine-node
  1. 安装 chromium 相关的包,这里切记切换为国内镜像源,不然你的安装速度巨慢,而且不一定成功!下面的 DockerFile 代码包含了安装依赖包(包含 chromium 和 字体相关的配置包等)和设置时区
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
    && sed -i '/dl-4.alpinelinux.org/d' /etc/apk/repositories \
    && apk update \
    && apk add tzdata \
    && apk add --update \
    && apk -U --no-cache update && apk -U --no-cache --allow-untrusted add \
      zlib-dev \
      xorg-server \
      dbus \
      chromium \
      bash \
      bash-doc \
      bash-completion -f \
      font-adobe-100dpi \
      fontconfig \
      xfonts-utils \
      dpkg \
      wget \
      unzip \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo "Asia/Shanghai" > /etc/timezone
  1. 依赖包安装成功后同样需要来搞字体的问题,这里我用 wget 来安装中文字体,并且通过 fc-cache 命令来更新字体
RUN cd /tmp && wget http://ftp.cn.debian.org/debian/pool/main/f/fonts-noto-cjk/fonts-noto-cjk_20170601+repack1-3+deb10u1_all.deb && \
    dpkg -i fonts-noto-cjk_20170601+repack1-3+deb10u1_all.deb    && \
    wget https://github.com/adobe-fonts/source-sans-pro/releases/download/2.040R-ro%2F1.090R-it/source-sans-pro-2.040R-ro-1.090R-it.zip && \
    unzip source-sans-pro-2.040R-ro-1.090R-it.zip && cd source-sans-pro-2.040R-ro-1.090R-it  && mv ./OTF /usr/share/fonts/  && \
    fc-cache -f -v

上面三部分的 DockerFile 基本就大功告成了,这个在我们的 k8s 下通过 Docker 部署完全 ok,最终跑出来的页面截图也不会有乱码的问题出现,这里贴出完整的 DockerFile :

FROM xxx.xxx.com/alpine-node

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
    && sed -i '/dl-4.alpinelinux.org/d' /etc/apk/repositories \
    && apk update \
    && apk add tzdata \
    && apk add --update \
    && apk -U --no-cache update && apk -U --no-cache --allow-untrusted add \
      zlib-dev \
      xorg-server \
      dbus \
      chromium \
      bash \
      bash-doc \
      bash-completion -f \
      font-adobe-100dpi \
      fontconfig \
      xfonts-utils \
      dpkg \
      wget \
      unzip \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo "Asia/Shanghai" > /etc/timezone

RUN cd /tmp && wget http://ftp.cn.debian.org/debian/pool/main/f/fonts-noto-cjk/fonts-noto-cjk_20170601+repack1-3+deb10u1_all.deb && \
    dpkg -i fonts-noto-cjk_20170601+repack1-3+deb10u1_all.deb    && \
    wget https://github.com/adobe-fonts/source-sans-pro/releases/download/2.040R-ro%2F1.090R-it/source-sans-pro-2.040R-ro-1.090R-it.zip && \
    unzip source-sans-pro-2.040R-ro-1.090R-it.zip && cd source-sans-pro-2.040R-ro-1.090R-it  && mv ./OTF /usr/share/fonts/  && \
    fc-cache -f -v

RUN fc-list :lang=zh

WORKDIR /opt/www/xxx-server
COPY . /opt/xxx-server

CMD npm run start

注意事项

在写 DockerFile 的时候需要注意 你的命令和你的系统是否匹配,比如说你的系统是 ubuntu 的 ,而你在这里写了 centos 的命令,这有些命令是会报错的从而导致你的部署失败。

关于镜像源强烈建议切换为国内镜像源,可以看看阿里云镜像源清华大学开源镜像站

关于中文字体,尽量选择去安装开源的中文字体。

结语

巡检服务对于前端页面有很多用处,可以做一些检测、自动化测试、产出性能报告等等,可以作为前端开发过程中一个有效的工具去进行应用,对于个人来说在技术的广度上也能得到很大的提升~~