Puppeteer 是Google Chrome团队开源的一个Nodejs库项目,提供一些高等级的API通过DevTools协议来操作一个无头Chrome浏览器,当然你也可以通过配置启动一个非无头的Chrome浏览器。
本文介绍下使用puppeteer在线上实现截图功能,并记录一些出现的问题及解决方法。本文同步发表于知乎专栏:前端微志。
可能很多人用过 PhantomJS,Puppeteer的很多功能跟PhantomJS类似。毕竟是Chrome官方出品,相信以后会同步更新Chrome的新功能特性,功能会越来越强大的。
你可以用它自动化地操作Chrome浏览器,官方给出了一些 Puppeteer 的使用场景:
网页截图快照及生成PDF文件;
爬取 SPA(单页面)系统,并生成 pre-rendered(预渲染) 内容(比如:SSR);
擦除页面内容;
自动化表填提交,UI测试,键盘输入等;
创建一个最新的,自动化测试环境,直接在最新版本的Chrome上使用最新的JavaScript和浏览器特性;
捕获你的网站的时间轴信息(timelime trace)帮助你诊断性能问题。
关于puppeteer,就简单介绍到这,GitHub上搜 puppeteer 了解详细的内容。
上周,产品经理抛过来一个需求:每周一,将系统内一个报表页面(包含图标和table等)截图并通过邮件发送给指定人员。
刚拿到这个需求的时候,觉得很麻烦,因为项目是用Vue做的单页面应用,要想截取项目页面,保证页面样式正常展示,页面上的11个使用 echarts 做的图标要正常渲染完展示。
由于前端项目是运行在 docker 的 Nginx 上的,通过反向代理访问Java后端的接口,当前架构不好实现截图这个功能。关于截图,与后端同事做了讨论,有三种方案:
在 Java 端使用 phantomjs 的插件,实现截图;
新建 Nodejs 服务,使用 phantomjs 访问系统页面截图;
新建 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';
// 启动puppeteer
puppeteer.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中启动起来,报这个错应该跟环境没关系,接着就各种调试,发现下面几种情况:
当截取一个简单页面(没有复杂图表等的渲染)的时候,不会报错,截图成功;
如果不设置页面尺寸,使用默认尺寸,不会报错,截图成功;
设置尺寸后,不等待数据加载,不延时等待,不会报错,截图成功;
加上等待延时,会报错,设置尺寸会报错,截图失败。
通过以上现象,首先想到的是线上docker运行内存太小,但是4G的内存已经不少了,再升级到8G内存之后,还是报错,就排除内存太小的原因。
再分析,会不会是浏览器Tab页宽度太大,导致Chrome在打开页面,渲染页面需要耗费的性能太大,导致Tab页崩溃?
抱着试试看的态度,调小的页面宽度,当把宽度调整为1200时,加上延时等待,截图成功了,这时的我本来也是一脸懵逼,WTF。
但是,还是存在一定概率的失败,差不多有十分之一的概率会失败,然后,又将宽度调整为1000时,基本上就不会报错,每次都能完美的截图。
这次试用puppeteer的经验,让我知道一个道理,出现问题了,什么原因都是有可能的,当你不确定时,就把可能的原因都考虑一下,那样可能就很容易找到问题所在了。
本来自认为不可能是宽度的问题,就以为可能是我的写法有问题,换了很多种方式,最后钻到了牛角里出不来,浪费了很多的时间,现在想来时间浪费得很不值得。
总的来说,puppeteer还是很强大的,用来做UI自动化测试,和一些小工具都是很不错的。各位,可以去发现puppeteer的更多的应用场景。