玩转Puppeteer

1 简介

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。

Puppeteer 默认以无头模式(headless)运行,也就是运行一个无界面的 Chrome 浏览器。

2 应用场景

2.1 页面生成 PDF

Puppeteer 提供了页面生成 PDF 的方法,我们可以利用这个方法来将页面导出为 PDF ,导出的 PDF 效果和 Chrome 浏览器打印功能导出的 PDF 一致。

具体的应用场景有:

报表导出 PDF在线文档导出 PDF

2.2 页面截图

Puppeteer 提供了截图的方法,我们可以利用这个方法来将页面的指定区域导出为 jpeg 或 png 图片。

具体的应用场景有:

报表导出图片在线文档导出图片营销页面生成预览图、封面图生成骨架屏图片

2.3 服务端渲染

单页应用(SPA)的主要内容是在 JavaScript 向服务端请求数据后渲染的,存在爬虫难以抓取主要内容、首屏加载慢等问题,而使用 Next.js、Nuxt.js 等服务端渲染框架改造的成本较高。

如果只是为了搜索引擎优化,我们可以考虑利用 Puppeteer 来实现。我们可以在网关层判断请求的来源,如果是爬虫,直接返回由 Puppeteer 服务端渲染的 html 文件。

2.4 自动化UI测试

使用 Puppeteer 可以模拟 Chrome 浏览器环境,结合 JavaScript 测试框架(如 Jest)可以实现自动化 UI 测试。

Puppeteer 提供了 Mouse 类来模拟鼠标操作,提供了 Keyboard 类来模拟键盘操作,提供了 Touchscreen 类来模拟触屏操作,并且 Puppeteer 提供的 Page 类里有很多方法可以用来操作元素,比如点击元素、聚焦元素等操作。

2.5 页面检测分析

使用 Puppeteer 提供的 page.tracing 系列方法捕获网站的 timeline trace 来对页面进行性能分析。

使用 Puppeteer 提供的 page.coverage 系列方法来获取 JavaScript 和 CSS 覆盖率。

使用 Puppeteer 提供的 page.metrics() 方法来获取某个时间点页面的指标数据,包括页面的 documents 数量、iframe 数量、js 事件数量、dom 节点数量、布局数量、样式重新计算数量、布局时间、样式重新计算总时间、js 代码执行总时间、任务执行总时间、占用堆内存大小、总的堆内存大小。

使用 Puppeteer 提供的 Request 类和 Response 类来监控页面发送的请求和接受的响应。

3 基础概念

Puppeteer API 是分层次的,反映了浏览器结构。

Puppeteer 使用 DevTools 协议与浏览器进行通信。

Browser 是浏览器实例,可以有多个浏览器上下文。

BrowserContext 是浏览器上下文实例,定义了一个浏览会话并可拥有多个页面。

Page 是页面实例,至少拥有一个框架(主框架mainFrame),可能还有由 iframe 创建的其他框架。

Frame 是框架实例,至少有一个默认的 JavaScript 执行上下文。可能还有与扩展插件关联的执行上下文。

Worker 表示一个WebWorker,具有单一执行上下文。

4 快速上手

4.1 安装 puppeteer-core

npm i puppeteer-core

puppeteer-core 是一个轻量级的 Puppeteer 版本,自 1.7.0 版本以来,官方都会发布一个 puppeteer-core 包,安装这个包时,默认不会下载 Chromium。

4.2 下载 Chromium

Puppeteer 官网:http://www.puppeteerjs.com/

chromium 下载链接:?path=chromium-browser-snapshots/

找到对应环境对应版本的 chromium 并下载。比如,我的环境是 Win_x64,安装的 Puppeteer 是 13.6.0 版本,查看 Puppeteer 的版本记录,发现对应的 chromium 是 。

4.3 启动无头浏览器

在项目根目录下新建一个 chrome 目录,在 chrome 目录下新建 win64- 目录,将下载的 chromium 解压放入其中。

在项目根目录下新建一个 src 目录,在 src 目录下新建 util 目录,在 util 目录下新建 browser.js文件。

"use strict"; const puppeteer = require("puppeteer-core"); const path = require("path"); const os = require("os"); function getExecutableFilePath() {   const extraPath = {     Linux: "linux-/chrome-linux/chrome", // linux     // Darwin: , // MacOs     Windows_NT: "win64-/chrome-win/chrome.exe", // windows   }[os.type()];   return path.join(path.join(__dirname, "../../chrome"), extraPath); } async function createBrowser(parmas = {}) {   const browser = await puppeteer.launch({     // headless: false,     executablePath: getExecutableFilePath(),     args: [       "--disable-dev-shm-usage", // 大量渲染时候写入/tmp而非/dev/shm,防止页面内存溢出崩溃       "--no-sandbox", // 禁用沙盒     ],     timeout: 0, // 禁止超时     ...parmas,   });   return browser; } module.exports = {   createBrowser,   getExecutableFilePath, };

上述代码简单封装了一个启动无头浏览器的方法,通过设置 chromium 路径的方式来启动方便在不同操作系统下启动不同的 chromium,并在启动时传入一些参数进行优化。

4.4 导出 HTML 文件

这里以导出 HTML 文件的小功能为例,介绍如何使用 Puppeteer。

在 src 目录下新建 example 目录, 在 example 目录下新建 exportHtml.js 文件。

"use strict"; const { createBrowser } = require("../util/browser.js"); const fs = require("fs"); const path = require("path"); (async function () {   const browser = await createBrowser();   const page = await browser.newPage();   await page.goto("");   const html = await page.content();   fs.writeFileSync(path.join(__dirname, "exportHtml.html"), html);   await browser.close(); })();

这里启动了一个无头浏览器,并创建了一个页面,去访问百度首页,然后导出网页的 HTML 源码并写入 exportHtml.html 文件中。

使用 node 运行这个 js 文件。

node ./src/example/exportHtml.js

运行完毕后,example 目录下出现了 exportHtml.html 文件。打开这个文件,就能看到静态的百度首页了。因为这里只导出了 HTML 源码,没有对其他资源进行处理,直接打开页面会有一些资源路径问题。

5 更多示例

5.1 网页导出 PDF 文件

网页导出 PDF 文件这个功能往往需要配合网页的打印样式(@media print{...})使用。

在 @media print {} 里编写的样式代码只在打印的时候生效,通过编写打印样式,我们可以对页面元素的样式进行调整,控制打印的效果。

这里我们以将 vue2 官方文档导出为 PDF 为例,介绍 Puppeteer 导出 PDF 的功能。

首先,我们打开 vue2 的官方文档()。使用 Chrome 浏览器的打印功能,在打印预览中我们可以看到打印效果和实际网页的内容并不一致。这是因为 vue2 的官方文档网页添加了一些打印样式。访问打印样式所在的文件()并搜索 @media print 就能明白为什么在打印预览中一些元素(如顶栏、侧边栏等)被隐藏或者样式不同了。

接下来进入正题,让我们使用 Puppeteer 来实现同样的打印(导出 PDF)功能。

在 example 目录下新建 exportPdf.js 文件。

"use strict"; const { createBrowser } = require("../util/browser.js"); const path = require("path"); (async function () {   const browser = await createBrowser();   const page = await browser.newPage();   await page.goto("");   await page.pdf({     path: path.join(__dirname, "./exportPdf.pdf"),   });   await browser.close(); })();

pdf 方法会返回 PDF 文件的 Buffer 数据,以便后续处理。这里我们只是演示一下这个功能,传入 path 参数就能让 pdf 方法将 PDF 文件写到指定路径了。

使用 node 运行这个 js 文件。

node ./src/example/exportPdf.js

运行完毕后,example 目录下出现了 exportPdf.pdf 文件。打开这个文件便能看到 vue2 官方文档了。

5.2 网页截图

在这一部分,我们演示一下整个网页截图的功能。

在 example 目录下新建 exportImg.js 文件。

"use strict"; const { createBrowser } = require("../util/browser.js"); const path = require("path"); (async function () {   const browser = await createBrowser();   const page = await browser.newPage();   await page.goto("");   await page.screenshot({     path: path.join(__dirname, "./exportImg.png"),     fullPage: true,   });   await browser.close(); })();

使用 node 运行这个 js 文件。

node ./src/example/exportImg.js

运行完毕后,example 目录下出现了 exportImg.png 文件。打开这个文件便能看到 vue2 官方文档了。