Puppeteer:从零出发,全面掌握浏览器自动化神器

1,446 阅读10分钟

我是小鑫同学,在北京工作的一位前端开发工程师。我擅长使用 Vue.jsAngularTypescriptNode.js 构建 Web 应用程序和网站。同时我也是一位乐于分享的程序员,我经常利用休息时间写写技术文章、分享自己经验及学习心得。

近两年主要利用 MicroApp 微前端框架维护和迭代公司的项目,保证历史的 Angular 工程逐步向 Vue.js 的平稳迁移。

座右铭: 😇 所有付出都将是沉淀,所有美好终会如期而至

框架介绍

Puppeteer 译为木偶,是一个 Node.js 库,内部通过 DevTools 协议提供控制 Chrome 或 Firefox 的一系列 API。通过定义可以看出 Puppeteer 的核心在于提供用户控制浏览器行为的方法,以下是一些自动化入门示例:

  • 自动提交表单、UI 测试、键盘输入等;
  • 使用最新的 JavaScript 和 浏览器特性创建自动化环境;
  • 捕获网站的时间线跟踪,帮助诊断性能问题;
  • 测试 Chrome 扩展程序;
  • 对页面截图和生成 PDF;
  • 对 SPA 应用爬取并生成预渲染内容;

安装指引

Puppeteer 从 v1.7.0+ 开始同时提供 puppeteerpuppeteer-core 两个包:

  • puppeteer 是在 puppeteer-core 基础上提供了更加完整的浏览器自动化产品:

    • 安装期间会下载与 Puppeteer 版本匹配的 Chrome for Testing;
    • puppeteer@v21.6.0+ 会同时下载 chrome-headless-shell 二进制文件;
    • 默认安装位置:$HOME/.cache/puppeteer
    • 提供合理的默认选项;
  • puppeteer-core 是通过 DevTools 协议提供编程接口驱动的核心库:

    • 安装期间不会下载 Chrome for Testing 及 chrome-headless-shell
    • 不提供任何默认选项;
npm i puppeteer # 完整版
npm i puppeteer-core # 核心库,需要显示指定远程/本地浏览器的连接地址

入门示例:

先快速初始化一个示例项目:

mkdir caniuse-puppeteer && cd caniuse-puppeteer
npm init -y && npm i puppeteer
echo 'console.log("Hello World!");' > index.mjs
npm pkg set type="module" 
npm pkg set scripts.dev="node index.mjs" 

PS:在使用 npm pkg set 时要注意 npm 的版本。

在示例中我尝试模拟用户在 caniuse.com 检索 Flexible 关键词,并打印出的第一条信息的描述内容:

import puppeteer from 'puppeteer';

(async () => {
  	// ① 启动浏览器并打开一个新的页签
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.setViewport({ width: 1920, height: 1080 })
  	// ② 跳转到 https://caniuse.com/ 地址
    await page.goto('https://caniuse.com/')
		
  	// ③ 通过 locator 定位 input 元素,并输入 Flexible 关键词
    await page.locator('input[name="search"]').fill('Flexible')
  	// ④ 等待查询结果对应的元素出现
    await page.waitForSelector('ciu-feature-list >>> .section__tables .feature-list-wrap ciu-feature >>> header div p')

  	// ⑤ 利用 evaluate 方法到目标页面的上下文获取第一条描述内容
    const textContent = await page.evaluate(() => {
        const element = document.querySelector("#main > main > ciu-feature-list")
            .shadowRoot.querySelector("#flexbox")
            .shadowRoot.querySelector("header > div > p")
        return element.textContent.trim()
    })
    console.log(textContent)
  
		// ⑥ 关闭浏览器
    await browser.close()
})()

PS:>>> 是 Puppeteer 提供的查询 Shadow DOM 元素的组合器。

运行环境:

  • Node 18+,Puppeteer 遵循 Node 最新维护的 LTS 版本;
  • 如果要使用 TypeScript,需要安装 TypeScript 4.7.4 +;
  • 可以在 Windows(x64)、MacOS(x64/arm64)、Debian/Ubuntu Linux(x64);

核心概念

Puppeteer 拥有 4 个核心概念,分别是:

核心概念描述
浏览器管理Puppeteer 提供了启动、关闭和连接已启动的浏览器等主要功能。
JavaScript 执行Puppeteer 在其驱动的页面上下文中执行 JavaScript 函数。
网络日志Puppeteer 默认监听所有的网络请求和响应,并在 page 上派发对应的事件
页面交互Puppeteer 允许使用鼠标、触摸事件和键盘输入与页面元素交互,通常应首先使用 CSS 选择器查询 DOM 元素。

浏览器管理:

入门示例中已经使用过了启动和关闭浏览器的 API,这里主要了解一下浏览器上下文(包含权限)和如何连接到正在运行的浏览器两部分。

  1. 浏览器上下文及上下文权限:

    • 浏览器上下文的作用是隔离自动换任务,保证 Cookie 和本地存储不会在浏览器上下文之间共享;
    • 浏览器上下文所关联的页面会在关闭上下文时一同被关闭;
    • 浏览器上下文支持权限配置,例如在首次访问高德地图需要提供 geolocation 权限;

    获取和创建浏览器上下文 API:

     // 获取默认的浏览器上下文
     await browser.defaultBrowserContext()
     
     // 创建一个新的浏览器上下文
     await browser.createBrowserContext()
    

    为默认浏览器上下文重写访问高德地图的定位权限:

    import puppeteer from 'puppeteer';
    
    (async () => {
        const browser = await puppeteer.launch({
            headless: false,
        })
        const context = await browser.defaultBrowserContext()
    
        const url = 'https://ditu.amap.com/'
        await context.overridePermissions(url, ['geolocation'])
    
        const page = await context.newPage()
        await page.goto(url)
    })()
    
  2. 如何连接到正在运行的浏览器:

    • 除了入门示例是用到的启动浏览器的方式外,还可以使用 connect 直接连接到已启动的浏览器。

    使用 launch 启动浏览器:

    import puppeteer from 'puppeteer';
    
    (async () => {
        const browser = await puppeteer.launch({
            headless: false,
            // 设置远程调试端口
            args: ['--remote-debugging-port=9222']
        })
        const page = await browser.newPage()
        // /json/version 获取 webSocketDebuggerUrl
        await page.goto('http://localhost:9222/json/version')
    })()
    

    连接上一个浏览器并打印 caniuse.com 网站的 news 元素:

    import puppeteer from 'puppeteer';
    
    (async () => {
        const browser = await puppeteer.connect({
            browserWSEndpoint: "ws://localhost:9222/devtools/browser/5bce4998-1ba5-4f96-b512-d0fb8d49187d"
        })
        const page = await browser.newPage()
        await page.goto('https://caniuse.com/')
        const textContext = await page.locator('.news').map(el => el.textContent).wait()
        console.log(textContext)
    
        // 断开连接并不会关闭浏览器
        await browser.disconnect()
    })()
    

JavaScript 执行:

在 Puppeteer 驱动的页面上下文中执行 JavaScript 函数同样在入门示例中有过使用,但没有提到如何传递参数和其中的一个缺陷。

传参:evaluate 第二个参数支持传递一个 ElementHandle 对象:

import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://caniuse.com/')

    const handle = await page.locator('.news').waitHandle()
    const textContent = await page.evaluate(el => el.textContent, handle)
    console.log(textContent)

    await browser.close()
})()

缺陷:上面示例中 textContent 被成功的输出,说明 el 是个有效的对象,但如果直接返回 el 对象,你会看到不一样的结果,终端输出了 {} 。造成这个现象的原因是 Puppeteer 会将对象序列化导致得到了不正确的结果,为了处理返回的对象,Puppeteer 提供了通过引用返回对象的方法:

import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://caniuse.com/')

    const handle = await page.locator('.news').waitHandle()
    const element = await page.evaluateHandle(el => el, handle)
    console.log(element instanceof ElementHandle)

    await browser.close()
})()

PS:在实际使用时要格外注意,多加测试。

网络日志:

page 提供了一个 on(event, handler) 函数,允许对 Puppeteer 派发的事件进行监听。

import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://caniuse.com/')

    page.on('request', request => {
        console.log('request : ', request.url())
    })

    page.on('response', response => {
        console.log('response : ', response.url())
    })
})()

页面交互:

前面的示例中或多或少都使用到了Puppeteer 提供与页面交互的 API,页面交互也是 Puppeteer 核心概念中内容最多的一块,所以放到这个小节的最后来讲。

定位器:

Puppeteer 推荐使用定位器 API 选择元素并与之交互,定位器 API 会等待元素在 DOM 中处于可操作的正确状态。但是如果定位器 API 无法满足时仍可以使用低级别的 API,如:page.waitForSelector()ElementHandle

普通操作:

操作类型API 示例默认检查项目
点击元素await page.locator('button').click();1 确保元素位于视口中
2 等待元素可见或隐藏
3 等待元素启用
4 等待元素在两个连续的动画帧上具有稳定边界框
录入文本await page.locator('input').fill('hello world');1 确保元素位于视口中
2 等待元素可见或隐藏
3 等待元素启用
4 等待元素在两个连续的动画帧上具有稳定边界框
鼠标悬停await page.locator('div').hover();1 确保元素位于视口中
2 等待元素可见或隐藏
3 等待元素在两个连续的动画帧上具有稳定边界框
滚动元素await page.locator('div').scroll({ scrollTop: 10, scrollLeft: 20 });1 确保元素位于视口中
2 等待元素可见或隐藏
3 等待元素在两个连续的动画帧上具有稳定边界框
等待元素可见await page.locator('.loading').wait();等待元素可见或隐藏

配置自检项:

await page.locator('button')
  .setEnsureElementIsInTheViewport(false) // 禁用后无法保证操作前元素位于视口中
  .setVisibility(null)                    // 设置忽略操作前检查元素可见或隐藏状态
  .setWaitForEnabled(false)               // 禁用后无法保证操作前元素可用
  .setWaitForStableBoundingBox(false)     // 禁用后将不等待元素在两个连续动画帧上具有稳定边界框
  .click();

配置超时时间:

await page.locator('button').setTimeout(5 * 1000).click();

PS:由于网页的响应速度存在差异,默认的超时时间不满足需要的情况下,可使用 setTimeout() 函数适当延长,超时时将抛出 TimeoutError 异常。

获取元素值或 ElementHandle

// 使用 map 函数将元素映射为 JavaScript 值,调用 wait() 将返回序列化的 JavaScript 值
const enabled = await page.locator('button').map(el => !el.disabled).wait();

// 调用 waitHandle() 函数返回 ElementHandle
const buttonHandle = await page.locator('button').waitHandle();
await buttonHandle.click();

添加过滤器:

await page.locator('button')
	.filter(el = el.innerText().includes('Click Me'))
  .click();

PS:通过过滤器来匹配所有按钮元素中符合特定文本的按钮元素。

添加事件监听:

await page.locator('button')
  .on(LocatorEvent.Action, () => {
      console.log('clicked');
  }).click();

PS:目前定位器仅支持一个单独的事件,事件会在定位器准备执行动作前触发,以此表示所有前提条件已经得到满足。

自定义等待函数:

await page.locator(() => {
    let resolve;
    const promise = new Promise((res) => {
        return (resolve = res)
    });
    const observer = new MutationObserver((records) => {
        for (const record of records) {
            if (record.target instanceof HTMLCanvasElement) {
                resolve(record.target);
            }
        }
    });
    observer.observe(document);
    return promise;
}).wait()

PS:示例中借助 MutationObserver 订阅 document 中出现 HTMLCanvasElement 元素的功能。

等待选择器:

等待选择器(waitForSelector)与定位器相比是一个较低级别的 API,允许等待元素在 DOM 中可用。如果操作失败不具备重试特性,且需要手动释放生成 ElementHandle 以防止内存泄漏。

import pprt from "puppeteer"

(async () => {
    const browser = await pprt.launch()
    const page = await browser.newPage()
    await page.goto("URL_ADDRESS")

    // 使用 waitForSelector 查询句柄
    const element = await page.waitForSelector("div > .class-name")
    await element.click();

    // 注意释放资源
    await element.dispose();

    await browser.close();
})()

立即选择器:

在明确已知元素位于页面上时,可以直接使用立即选择器。

API描述
page.$()返回与选择器匹配的单个元素
page.$$()返回与选择器匹配的多个元素
page.$eval()返回与选择器匹配的第一个元素上运行 JavaScript 函数的结果
page.$$eval()返回与选择器匹配的每一个元素上运行 JavaScript 函数的结果

扩展选择器:

XPath 选择器(-p-path):

import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://developer.mozilla.org/zh-CN/')
      
  	// XPath 选择器
    const textContent = await page.locator('::-p-xpath((//*[@class="tile-container"]/div/h3/a)[1])')
      .map(el => el.textContent)
      .wait()
    console.log(textContent)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

Text 选择器(-p-text):

import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://developer.mozilla.org/zh-CN/')
      		
  	// 文本选择器
    const textContent = await page.locator('::-p-text(Developer essentials: JavaScript console methods)')
    	.map(el => el.textContent)
    	.wait()
		console.log(textContent)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

ARIA 选择器(-p-aria):

import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://developer.mozilla.org/zh-CN/')
      
  	// 无障碍属性选择器
    const innerHTML = await page.locator('::-p-aria(MDN homepage)')
      .map(el => el.innerHTML)
      .wait()
    console.log(innerHTML)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

Pierce 选择器(pierce/):

import pptr from 'puppeteer'
      
(async () => {
    const browser = await pptr.launch({ headless: false })
    const page = await browser.newPage()
    await page.setViewport({ width: 1080, height: 1024 })
    await page.goto('https://mdn.github.io/web-components-examples/composed-composed-path/')
        
  	// 穿透 shadow DOM 选择器,深度组合器,同 pierce/p
    const textContent = await page.locator('& >>> p')
    	.map(el => el.textContent)
    	.wait()
    console.log(textContent)
      
    await page.screenshot({ path: 'screenshot.png' })
    await browser.close()
})()

自定义选择器(如 -p-class):

import puppeteer, { Puppeteer } from "puppeteer"
      
(async () => {
    // 注册 class 选择器 
    Puppeteer.registerCustomQueryHandler('class', {
        queryOne: (node, selector) => {
            return node.querySelector(`.${selector}`)
        },
        queryAll: (node, selector) => {
            return [...node.querySelectorAll(`.${selector}`)]
        }
    })
      
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto("https://developer.mozilla.org/zh-CN/")
      
    // 使用 class 选择器
    const innerHTML = await page.locator('::-p-class(tile-title)').map(el => el.innerText).wait()
    console.log(innerHTML)
      
    await browser.close()
})()

配置说明

Puppeteer 支持通过配置文件和环境变量两种方式来改变默认配置项,且环境变量的优先级要高于配置文件。配置文件支持的格式如下:

  • .puppeteerrc.cjs
  • .puppeteerrc.js
  • .puppeteerrc (YAML、JSON)
  • .puppeteerrc.json
  • .puppeteerrc.config.js
  • .puppeteerrc.config.cjs

配置文件使用示例:

const { join } = require('path');

/**
 * @type {import('puppeteer').Configuration}
 */
module.exports = {
  	// 修改缓存目录后需要重新安装 Puppeteer,以保证新的缓存目录中包含的运行的必要文件
    cacheDirectory: join(__dirname, '.cache', 'puppeteer')
}

配置选项及对应的环境变量:

配置项类型环境变量描述
browserRevisionstringPUPPETEER_BROWSER_REVISION指定浏览器版本号,默认值为当前 Puppeteer 内置的浏览器版本号
cacheDirectorystringPUPPETEER_CACHE_DIR指定 Puppeteer 使用的缓存目录,默认通过 path.join(os.homedir(), ‘.cache’, ‘puppeteer’) 配置路径
defaultProductchrome、firefoxPUPPETEER_PRODUCT指定浏览器产品,默认为 chrome 浏览器
downloadBaseUrlstringPUPPETEER_DOWNLOAD_BASE_URL指定下载浏览器的前缀地址,不同的浏览器产品对应的下载路径不同:storage.googleapis.com/chrome-for-… or archive.mozilla.org/pub/firefox…
executablePathstringPUPPETEER_EXECUTABLE_PATH指定 puppeteer.launch 启动路径,默认会自动查找安装路径
experimentsRecord<string, never>--指定 Puppeteer 的实验选项
logLevelsilent、error、warn--指定日志输出级别,默认为 warn 级别
skipChromeDownloadbooleanPUPPETEER_SKIP_CHROME_DOWNLOAD安装 Puppeteer 时跳过 Chrome 下载
skipChromeHeadlessShellDownloadbooleanPUPPETEER_SKIP_CHROME_HEADLESS_SHELL_DOWNLOAD安装 Puppeteer 时跳过 chrome-headless-shell 下载
skipDownloadbooleanPUPPETEER_SKIP_DOWNLOAD安装 Puppeteer 时跳过下载
temporaryDirectorystringPUPPETEER_TMP_DIR指定 Puppeteer 使用的临时文件目录,默认通过 os.tmpdir() 配置路径

PS:环境变量还包含 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 用于定义下载和运行浏览器的代理设置。

调试说明

由于 Puppeteer 设计浏览器的许多不同组件,因此没有统一的方式调试所有的可能得问题,Puppeteer 尽可能的提供多种调试方法来涵盖所有可能得问题。

一般来说在使用 Puppeteer 的时候主要的问题来自两个来源:在 Node.js 上运行的代码(称之为服务端代码)和在浏览器端运行的代码(称之为客户端代码)。

基础配置:

因为调试往往发生在开发环境中,所以提供一个环境变量来动态启动调试的基础配置还是有很帮助的:

  1. 禁用无头模式:可以查看浏览器显示的内容,主观的观察内容变化;
  2. 延长执行时间:通过延长执行时间来观察正在发生的情况;
  3. 启用浏览器调试:调试时会自动启动开发者工具;
  4. 打印浏览器日志:启用后可以接管浏览器意外崩溃或无法正常启动时的日志信息。
import puppeteer from 'puppeteer'

const production = process.env.NODE_ENV === 'production';

(async () => {
    const browser = await puppeteer.launch({
        // 开发环境中不使用无头模式
        headless: production,
        // 开发环境中延长执行时间
        slowMo: production ? 0 : 250,
        // 开发环境中打开开发者工具
        devtools: production ? false : true,
        // 开发环境中输出浏览器进程信息
        dumpio: production ? false : true,
    })
})()

客户端代码调试:

  1. 捕获客户端代码中的 console.* 的输出:

    // 监听页面的 console.* 输出 
    page.on('console', msg => {
        console.log('PAGE LOG: ', msg.text())
    })
       
    // 模拟浏览器环境中 console.* 输出
    await page.evaluate(() => {
        console.log('PAGE EVAL: ', 'Hello World!')
    })
    
  2. 添加 debugger; 关键字中断代码:

    // 注意启用 devtools 选项
    await page.evaluate(() => {
      	// 模拟客户端代码中使用 debugger; 关键字中断代码执行
      	debugger;
        console.log('PAGE EVAL: ', 'Hello World!')
    })
    

服务端代码调试:

在 Node.js 中使用调试器仅限于 Chrome 和 Chromium 中使用。

  1. 在关闭无头模式的前提下,需要在运行服务端代码的脚本中添加 --inspect-brk 选项,如:
npm pkg set scripts.debug="cross-env NODE_ENV=development node --inspect-brk index.mjs" // v7.24.2 +
  1. 在 Chrome 或 Chromium 中打开 chrome://inspect/#devices ,在新页面中的 Remote Target 菜单下找到对应的 Target 并启动调试。
  2. 在新打开的浏览器中,按 F8 可以恢复测试执行;
  3. 添加的 debugger; 关键字也会被命中并中断程序执行;

记录 DevTools 协议流量:

以上的调试方法都不起作用时,则可能是 Puppeteer 和 DevTools 协议之间可能存在着问题,那这时候可以通过设置 DEBUG 环境变量来进一步调试:

# 基本详细日志记录
cross-env DEBUG="puppeteer:*" node script.js

# 防止截断长消息
cross-env DEBUG="puppeteer:*" env DEBUG_MAX_STRING_LENGTH=null node script.js

# 协议通信可能相当繁杂。此示例过滤掉所有网络域消息
cross-env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'

# 过滤掉所有协议消息,但保留所有其他日志记录
cross-env DEBUG="puppeteer:*,-puppeteer:protocol:*" node script.js

记录待处理的协议调用:

如果遇到 Puppeteer 异步任务未能变为 Fulfilled 状态时,可以尝试使用 debugInfo 借口记录被挂起的回调,并查看导致的原因:

console.log(browser.debugInfo.pendingProtocolErrors);

请求拦截

调用 await page.setRequestInterception(true) 主动启用请求拦截,启用后每个请求都将被停止,除非主动将请求切换为继续、响应或中止状态。

传统模式

示例中访问了 taobao 主页,并启用的请求拦截,当请求 url 包含 .png.jpg 后缀时,请求将被中止:

import puppeteer from 'puppeteer';

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    const page = await browser.newPage();
    await page.setViewport({ width: 1920, height: 1080 });
    await page.setRequestInterception(true);
    page.on('request', request => {
        // 判断是否已经处理过请求
        if (request.isInterceptResolutionHandled()) return;
        if (
            request.url().endsWith('.png') ||
            request.url().endsWith('.jpg')
        )
            request.abort(); // 拦截中止
        else request.continue(); // 继续请求
    });
    await page.goto('https://taobao.com');
    await browser.close();
})();

PS:在处理拦截到的请求前要调用 isInterceptResolutionHandled()interceptResolutionState() 检查请求的状态,处理过的请求被再次处理会引发Request is already handled! 异常。

协作拦截模式

协作拦截主要在存在多个请求拦截处理的时候通过给 request.abortrequest.continuerequest.respond 设置可选的 priority 来调控它们的处理顺序。

协作拦截模式规则:

  1. 所有处理程序都必须提供优先级(priority)数值;
  2. 如果为提供优先级数值,则”传统模式“处于活动状态,而”协作拦截模式“处于非活动状态;
  3. 异步处理程序会在最终处理程序截获之前完成;
  4. 最高优先级的处理函数会被执行,但遇到优先级相同时,将按 abort > respond > continue 顺序执行;

PS:在指定协作拦截模式时,除非要设置更高的优先级,否则请使用 0 或 HTTPRequest.DEFAULT_INTERCEPT_RESOLUTION_PRIORITY

示例演示了传统模式占据最高优先级,请求会立即中止,因为在解析拦截器时只有有一个处理程序省略了 priority

page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;
  request.continue({}, 0);
});

page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;

  // 传统模式:立即中止
  request.abort('failed');
});

PS:此示例将在控制台收到类似 Error: net::ERR_FAILED at https://taobao.com 的异常信息,因为请求全部被中止掉了,更多的优先级示例见 pptr.dev/guides/netw…

Chrome 扩展测试:

Puppeteer 可以用于测试 Chrome 扩展程序,但需要注意的是 headless: 'shell' 模式中不可用。

首先准备一个仅包含 service_worker 的后台脚本,并配置好 manifest.json

{
    "name": "Hello World",
    "version": "0.1",
    "manifest_version": 3,
    "background": {
        "service_worker": "background.js"
    }
}
// background.js
console.log("background.js loaded");

将插件放到项目目录的 my-extension 文件夹中,接着通过配置 args 选项,加载插件:

import puppeteer from 'puppeteer'
import path from 'path'
import process from 'process'

const extensiondir = path.join(process.cwd(), 'my-extension');

(async () => {
    const browser = await puppeteer.launch({
        args: [
            `--disable-extensions-except=${extensiondir}`,
            `--load-extension=${extensiondir}`,
        ],
    })

    await browser.close();
})()

最后通过 evaluate() 函数在后台脚本中通过 chrome.runtime.getManifest() 获取插件的版本信息:

const workerTarget = await browser.waitForTarget(
    target => target.type() === 'service_worker' && target.url().endsWith('background.js')
);
const worker = await workerTarget.worker();
const version = await worker.evaluate(() => chrome.runtime.getManifest().version);
console.log(version);

PS:Puppeteer 文档显示目前尚无法测试扩展程序的内容脚本。

更多功能

屏幕截图:

要捕获屏幕截图可以使用:

import puppeteer from 'puppeteer'

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://developer.mozilla.org/zh-CN/', {
      	// Waits till there are no more than 2 network connections for at least `500` ms.
        waitUntil: 'networkidle2'
    })
    await page.screenshot({
        path: 'screenshot.png',
    });
    await browser.close();
})()

要捕获特定元素的截图可以使用:

const element = await page.waitForSelector('div');
await element.screenshot({
  path: 'screenshot.png',
});

默认情况下,如果元素处于 hidden 状态,ElementHandle.screenshot() 会尝试将其滚动到视图中。

PDF 生成:

要打印 PDF 可以使用 page.pdf() 方法,默认情况下这个方法会等待字体文件的加载。

import puppeteer from 'puppeteer'

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto('https://developer.mozilla.org/zh-CN/', {
        waitUntil: 'networkidle2'
    })
    await page.pdf({
        path: 'mozilla.pdf',
    });
    await browser.close();
})()

Cookies:

Puppeteer 提供了设置 Cookie 的函数 await page.setCookie({}) 和提取页面所设置的 Cookie 的函数 await page.cookies()

文件上传:

Puppeteer 不提供以编程方式处理文件下载的方法,要上传文件,需要找到一个文件输入元素并调用 ElementHandle.uploadFile('./local-file')

总结

综上所述,Puppeteer 作为一款功能全面的浏览器自动化工具,为网页抓取、自动化测试和浏览器操作提供了坚实基础。无论是自动填写表单、捕获性能数据,还是生成页面截图和PDF,Puppeteer 都以其丰富的API和强大的控制能力,助力开发者实现自动化需求。掌握Puppeteer,意味着解锁了网页自动化世界的无限可能,为你的开发工作带来更高效率和更多创新。希望本文能成为你驾驭Puppeteer的起点,开启自动化之旅的精彩篇章。

PS:本文内容及示例代码部分直接来自『Puppeteer』文档,也是对阅读文档几天下来的一份总结,后续有时间会补充 Puppeteer 在 Chrome 或 Chrome Extension 运行的案例。