使用puppeteer截图及线上问题调试

3,248 阅读6分钟
原文链接: mp.weixin.qq.com

Puppeteer 是Google Chrome团队开源的一个Nodejs库项目,提供一些高等级的API通过DevTools协议来操作一个无头Chrome浏览器,当然你也可以通过配置启动一个非无头的Chrome浏览器。本文介绍下使用puppeteer在线上实现截图功能,并记录一些出现的问题及解决方法。

本文同步发表于知乎专栏:前端微志。

简单介绍puppeteer

可能很多人用过 PhantomJS,Puppeteer的很多功能跟PhantomJS类似。毕竟是Chrome官方出品,相信以后会同步更新Chrome的新功能特性,功能会越来越强大的。

你可以用它自动化地操作Chrome浏览器,官方给出了一些 Puppeteer 的使用场景:

  1. 网页截图快照及生成PDF文件;

  2. 爬取 SPA(单页面)系统,并生成 pre-rendered(预渲染) 内容(比如:SSR);

  3. 擦除页面内容;

  4. 自动化表填提交,UI测试,键盘输入等;

  5. 创建一个最新的,自动化测试环境,直接在最新版本的Chrome上使用最新的JavaScript和浏览器特性;

  6. 捕获你的网站的时间轴信息(timelime trace)帮助你诊断性能问题。

关于puppeteer,就简单介绍到这,GitHub上搜 puppeteer 了解详细的内容。

功能分析

上周,产品经理抛过来一个需求:每周一,将系统内一个报表页面(包含图标和table等)截图并通过邮件发送给指定人员。

刚拿到这个需求的时候,觉得很麻烦,因为项目是用Vue做的单页面应用,要想截取项目页面,保证页面样式正常展示,页面上的11个使用 echarts 做的图标要正常渲染完展示。

由于前端项目是运行在 docker 的 Nginx 上的,通过反向代理访问Java后端的接口,当前架构不好实现截图这个功能。关于截图,与后端同事做了讨论,有三种方案:

  1. 在 Java 端使用 phantomjs 的插件,实现截图;

  2. 新建 Nodejs 服务,使用 phantomjs 访问系统页面截图;

  3. 新建 Nodejs 服务,使用 puppeteer 实现截图功能;

通过比较, phantomjs 截图的效果不太好,页面样式显示不够精细,考虑到后期还会使用到 Nodejs 来实现其他功能,最后决定采用方案三,将 Nodejs 服务作为一个单独的服务层,只提供截图功能的服务,暴露出接口出去,供 Java后端调用,且定时发送邮件的功能交给 Java后端完成。

Nodejs 服务使用 Express 提供路由功能,暴露出接口供外部调用。

功能流程

Java后端定时调用 Nodejs 接口 → puppeteer截图页面 → 截图后将图片上传到图片服务器 → 图片上传成功后,将图片信息返回给Java后端 → Java后端从库中捞取对应的截图并发送邮件

截图实践

说了这么多,贴一下代码(Nodejs v7.6及以上,支持async、await语法,如Nodejs版本较低,可使用Promise写法)。

// 引入依赖插件const puppeteer = require('puppeteer');const fs = require('fs');const path = require('path');const request = require('request');let theBrowser = null;const websiteUrl = 'example.com/dashboard';const uploadFileUrl = 'upload.com/upload';// 启动puppeteerpuppeteer.launch({    // root 权限下需要取消sandbox
  args: ['--no-sandbox']
}).then(async browser => {
  theBrowser = browser    // 打开浏览器后,新建tab页
  const page = await browser.newPage();      // 设置tab页的尺寸,puppeteer允许对每个tab页单独设置尺寸
  await page.setViewport({
    width: 1000,
    height: 3480
  });      // tab访问需要截图的页面,使用await可以等待页面加载完毕
  await page.goto(websiteUrl);      // 由于页面数据是异步的,所以等待8秒,等待异步请求完毕,页面渲染完毕
  await page.waitFor(8000);      // 页面渲染完毕后,开始截图
  await page.screenshot({
    path: './dashboard_shot.png',
    clip: {
      x: 200,
      y: 60,
      width: 780,
      height: 3405
    }
  });      // 截图成功后,将截图上传至图片服务器
  request.post({
    url: uploadFileUrl,
    headers: {            // 此处模拟一个服务器校验token
      userToken: '2361A77FDD432C6B464C57007C062B82'
    },
    formData: {
      file: fs.createReadStream(        path.join(__dirname, './dashboard_shot.png')      )
    }
  }, (err, httpResponse, body) => {        // 异常处理,且不管失败还是成功,都关闭打开的浏览器
    if (err) {
      theBrowser.close();
      fs.unlink(        path.join(__dirname, './dashboard_shot.png')      )            return console.error('upload failed:', err)
    }

    fs.unlink(      path.join(__dirname, './dashboard_shot.png')    )
    theBrowser.close();
  })
}).catch(error => {
  theBrowser.close();
});

遇到的问题

最开始,设置 page 的宽度是1700,在docker上的Ubuntu服务器上部署成功后,运行的时候会报错,报错信息是:Page crashed!(页面崩溃)

当报这个错的时候,我是一脸懵逼的,因为在本机运行是OK的,本地环境是MAC OS,按理说,既然能在线上docker中启动起来,报这个错应该跟环境没关系,接着就各种调试,发现下面几种情况:

  1. 当截取一个简单页面(没有复杂图表等的渲染)的时候,不会报错,截图成功;

  2. 如果不设置页面尺寸,使用默认尺寸,不会报错,截图成功;

  3. 设置尺寸后,不等待数据加载,不延时等待,不会报错,截图成功;

  4. 加上等待延时,会报错,设置尺寸会报错,截图失败。

通过以上现象,首先想到的是线上docker运行内存太小,但是4G的内存已经不少了,再升级到8G内存之后,还是报错,就排除内存太小的原因。

再分析,会不会是浏览器Tab页宽度太大,导致Chrome在打开页面,渲染页面需要耗费的性能太大,导致Tab页崩溃?

抱着试试看的态度,调小的页面宽度,当把宽度调整为1200时,加上延时等待,截图成功了,这时的我本来也是一脸懵逼,WTF。

但是,还是存在一定概率的失败,差不多有十分之一的概率会失败,然后,又将宽度调整为1000时,基本上就不会报错,每次都能完美的截图。

总结

这次试用puppeteer的经验,让我知道一个道理,出现问题了,什么原因都是有可能的,当你不确定时,就把可能的原因都考虑一下,那样可能就很容易找到问题所在了。

本来自认为不可能是宽度的问题,就以为可能是我的写法有问题,换了很多种方式,最后钻到了牛角里出不来,浪费了很多的时间,现在想来时间浪费得很不值得。

总的来说,puppeteer还是很强大的,用来做UI自动化测试,和一些小工具都是很不错的。各位,可以去发现puppeteer的更多的应用场景。