介绍
Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过DevTools 协议控制 Chrome 或 Chromium 。Puppeteer 默认运行无头,但可以配置为运行完整(非无头)Chrome 或 Chromium。
一般用于:
生成页面的屏幕截图和 PDF。
抓取 SPA(单页应用程序)并生成预渲染内容(即“SSR”(服务器端渲染))。
自动化表单提交、UI 测试、键盘输入等。
创建最新的自动化测试环境。使用最新的 JavaScript 和浏览器功能直接在最新版本的 Chrome 中运行测试。
捕获您网站的时间线轨迹以帮助诊断性能问题。
测试 Chrome 扩展程序。
安装
1npm i puppeteer
截图
1// 截图2const screenShot = async () => {3 const browser = await puppeteer.launch();4 const page = await browser.newPage();5 await page.goto('https://example.com');6 const a = await page.screenshot({ path: 'example.png' });7 await browser.close();8}
执行scripts
1// 执行脚本2const doScript = async () => {3 await page.goto('https://www.all1024.com', {4 waitUntil: 'networkidle2',5 });6 await page.pdf({ path: '1024.pdf', format: 'a4' });7 const dimensions = await page.evaluate(() => {8 return {9 width: document.documentElement.clientWidth,10 height: document.documentElement.clientHeight,11 deviceScaleFactor: window.devicePixelRatio,12 };13 });14 console.log('Dimensions:', dimensions);15}
自动填表单
1// 自动填表单2const fillForm = async () => {3 const browser = await puppeteer.launch();4 // 调试可见5 // const browser = await puppeteer.launch({6 // headless: false,7 // executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',8 // args: ['--start-maximized']9 // //launch这里将浏览器设置为非无头模式,且这里设置启动本机安装的chrome,如果这里不设置,还需要下载chromium,这里请设置你自己本机的chrome浏览器10 // });11 const page = await browser.newPage();12 // 地址栏输入网页地址13 await page.goto('https://baidu.com/', {14 waitUntil: 'networkidle2',15 });1617 // 输入搜索关键字18 await page.type('#kw', '腾讯公司', {19 delay: 1000, // 控制 keypress 也就是每个字母输入的间隔20 });2122 // 回车23 await page.keyboard.press('Enter');24}
REPL(交互式解释器)
调试puppeteer应用程式有两种方法,一种是打开可见的带头的浏览器,观察真实效果,另一种是单步调试,通过交互式解释器来实现。
使用交互式 REPL 使快速 puppeteer 调试和探索变得有趣。
可以随时中断您的代码以在您的控制台中启动交互式REPL 。
向和实例添加便利.repl()方法。PageBrowser
支持检查任意对象和实例。
可用对象属性的功能选项卡自动完成和彩色提示。
1const puppeteer = require('puppeteer-extra');2const repl = require('puppeteer-extra-plugin-repl')();34async function showREPL() {5 await puppeteer.use(repl);6 //固定写法,表示puppeteer要使用repl插件78 const browser = await puppeteer.launch({9 headless: false,10 executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',11 args: ['--start-maximized']12 //launch这里将浏览器设置为非无头模式,且这里设置启动本机安装的chrome,如果这里不设置,还需要下载chromium,这里请设置你自己本机的chrome浏览器13 });14 const page = await browser.newPage();15 await page.setViewport({16 width: 1366,17 height: 76818 });19 await page.goto('https://example.com');20 //默认打开上面的网页2122 await page.repl();23 //在page对象上开启交互的REPL,这样可以实时看到page上提供的方法执行结果,执行效果见下图所示2425 //在page对象上开启交互的REPL,这样可以实时看到page上提供的方法执行结果2627 await browser.repl();28 //在browser对象上开启交互的REPL,这样可以实时看到page上提供的方法执行结果2930 await browser.close();31}32showREPL();
应用
最近公司会用到邮件发送报表的功能,来实现一个服务端生成网页png/pdf的功能吧
暴露接口,供其他服务调用
1const express = require("express");2const getPDF = require('./getPDF');3const getPNG = require('./getPNG');45const app = express();6const port = 9000;78app.get("/getPDF", async (req, res) => {9 const { url } = req.query;10 if (!url) { res.send({ status: 'error', msg: '缺少参数url' }) }11 const result = await getPDF({ url });12 res.send(result);13});1415app.get("/getPNG", async (req, res) => {16 const { url, width: _width } = req.query;17 if (!url) { res.send({ status: 'error', msg: '缺少参数url' }) }18 const width = _width ? 1000 : Number(_width);19 const result = await getPNG({ url, width });20 res.send(result);21});2223app.listen(port, () => {24 console.log(`Example app listening at http://localhost:${port}`);25});
getPNG实现
1const puppeteer = require('puppeteer');2const UTILS = require('./utils');34module.exports = async ({ url, name = 'file.png', width = 1366 }) => {5 const browser = await puppeteer.launch();6 const page = await browser.newPage();7 await page.setViewport({8 width,9 height: 76810 });11 await page.goto(url, {12 waitUtil: 'networkidle2'13 });14 await UTILS.waitTillHTMLRendered(page);15 console.log('开始截图……')16 const res = await page.screenshot({ path: name, fullPage: true });17 console.log('完成截图')18 await browser.close();19 return res20}
getPDF实现
1const puppeteer = require('puppeteer');23module.exports = async function getPDF({ url }) {4 const browser = await puppeteer.launch({5 headless: true,6 args: [7 "--no-sandbox", // linux系统中必须开启8 "--no-zygote",9 // "--single-process", // 此处关掉单进程10 "--disable-setuid-sandbox",11 "--disable-gpu",12 "--disable-dev-shm-usage",13 "--no-first-run",14 "--disable-extensions",15 "--disable-file-system",16 "--disable-background-networking",17 "--disable-default-apps",18 "--disable-sync", // 禁止同步19 "--disable-translate",20 "--hide-scrollbars",21 "--metrics-recording-only",22 "--mute-audio",23 "--safebrowsing-disable-auto-update",24 "--ignore-certificate-errors",25 "--ignore-ssl-errors",26 "--ignore-certificate-errors-spki-list",27 "--font-render-hinting=medium",28 ]29 });30 // try...catch...31 try {32 const page = await browser.newPage();33 await page.goto(url, {34 waitUtil: 'networkidle2'35 });36 // 页眉模板(图片使用base64,此处的src的base64为占位值)37 const headerTemplate = ``38 // 页脚模板(pageNumber处会自动注入当前页码)39 const footerTemplate = ``;40 // 对于大的PDF生成,可能会时间很久,这里规定不会进行超时处理41 await page.setDefaultNavigationTimeout(0);42 // 定义html内容43 // await page.setContent(this.HTMLStr, { waitUntil: "networkidle2" });44 // 等待字体加载响应45 await page.evaluateHandle("document.fonts.ready");46 let pdfbuf = await page.pdf({47 // 页面缩放比例48 scale: 1,49 // 是否展示页眉页脚50 // displayHeaderFooter: true,51 // pdf存储单页大小52 format: "a4",53 // 页面的边距54 // 页眉的模板55 // headerTemplate,56 // // 页脚的模板57 // footerTemplate,58 margin: {59 top: 0,60 bottom: 0,61 left: 0,62 right: 063 },64 // 输出的页码范围65 pageRanges: "",66 // CSS67 preferCSSPageSize: true,68 // 开启渲染背景色,因为 puppeteer 是基于 chrome 浏览器的,浏览器为了打印节省油墨,默认是不导出背景图及背景色的69 // 坑点,必须加70 printBackground: true,71 });72 // 关闭browser73 await browser.close();74 // 返回的是buffer不需要存储为pdf,直接将buffer传回前端进行下载,提高处理速度75 return pdfbuf76 } catch (e) {77 await browser.close();78 throw e79 }80}
判断页面加载完成工具函数
如何判断页面渲染完成其实不能单单从网络或者document load来判断,因为应用里js一般都会是动态加载其他js来动态渲染,所以,用定时轮询页面大小变化来判断页面是否加载完毕比较合适。
1// 判断页面加载完成2const waitTillHTMLRendered = async (page, timeout = 10000) => {3 console.log('页面加载中...')4 const checkDurationMsecs = 1000;5 const maxChecks = timeout / checkDurationMsecs;6 let lastHTMLSize = 0;7 let checkCounts = 1;8 let countStableSizeIterations = 0;9 const minStableSizeIterations = 3;10 while (checkCounts++ <= maxChecks) {11 let html = await page.content();12 let currentHTMLSize = html.length;13 let bodyHTMLSize = await page.evaluate(() => document.body.innerHTML.length);14 console.log('last: ', lastHTMLSize, ' <> curr: ', currentHTMLSize, " body html size: ", bodyHTMLSize);15 if (lastHTMLSize != 0 && currentHTMLSize == lastHTMLSize)16 countStableSizeIterations++;17 else18 countStableSizeIterations = 0; //reset the counter1920 if (countStableSizeIterations >= minStableSizeIterations) {21 console.log("页面完成加载!");22 checkCounts = maxChecks + 223 break;24 }25 lastHTMLSize = currentHTMLSize;26 await page.waitForTimeout(checkDurationMsecs);27 }28};2930313233module.exports = {34 waitTillHTMLRendered35}
至此,就实现了一个非常常见的需求,生成的png或者pdf就可以用于其他服务来发报表邮件了。