Puppeteer 的 Browser.extensions():获取浏览器已安装扩展清单的实现原理与实战用法
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
本文以 Puppeteer API 文档中的Browser.extensions()方法为核心,讲解它的签名、返回值Map<string, Extension>的结构,以及背后的Extension实例能提供哪些能力(扩展 ID、版本、名称、路径、启停状态,以及 service worker、扩展页面和动作触发)。结合仓库中的 CDP 协议实现、BiDi 实现限制与测试用例,读完后你不仅知道如何列出浏览器中的扩展,还能理解该方法的底层调用链、缓存机制及其适用前提。
方法签名与返回值
API 文档 对该方法的定义非常简洁:它用于获取浏览器中所有已安装扩展的映射表,其中键是扩展 ID,值是对应的 Extension 实例。
class Browser { abstract extensions(): Promise<Map<string, Extension>>; }返回值:Promise<Map<string, Extension>>
从签名可以看出三个要点:
- 异步方法:返回 Promise,必须
await后才能拿到 Map; - 键值结构:以扩展 ID(Chrome 中形如 32 位随机字母串的
extensionId)为键,天然支持extensions.get(extensionId)直接按键取用; - 抽象方法:
Browser是抽象类,该方法声明为abstract,具体行为由各协议(CDP / BiDi)的子类实现,这也是不同协议间行为差异的根源(见后文 BiDi 限制一节)。
在 抽象接口源码 中可以看到完整定义:
/** * Retrieves a map of all extensions installed in the browser, where the keys * are extension IDs and the values are the corresponding {@link Extension} instances. * * @public */ abstract extensions(): Promise<Map<string, Extension>>;返回值中的 Extension 实例
extensions()返回的每个值都是 Extension 抽象类的实例。该类的构造器标记为内部(@internal),第三方代码不应直接 new 或继承它,实例完全由 Puppeteer 内部根据协议响应创建。
Extension 提供 5 个只读属性和 3 个实例方法(见 Extension 文档):
| 成员 | 类型 | 说明 |
|---|---|---|
id | string | 扩展的唯一标识符 |
version | string | 扩展 manifest 中声明的版本号 |
name | string | 扩展 manifest 中声明的名称 |
path | string | 扩展在文件系统中的路径 |
enabled | boolean | 扩展是否处于启用状态 |
workers() | Promise<WebWorker[]> | 当前活跃的扩展 service worker 列表 |
pages() | Promise<Page[]> | 当前活跃且可见的扩展页面列表 |
triggerAction(page) | Promise<void> | 在指定页面上触发扩展默认动作(模拟点击工具栏动作图标) |
在 Extension 源码 中,构造器对id和version做了非空校验,缺失任一项会直接抛出Extension ID and version are required错误,说明这两个字段是扩展身份的最小必需集:
constructor( id: string, version: string, name: string, path: string, enabled: boolean, ) { if (!id || !version) { throw new Error('Extension ID and version are required'); } // ... }CDP 实现:Extensions.getExtensions 与实例缓存
extensions()的 CDP 协议实现位于 CdpBrowser:
override async extensions(): Promise<Map<string, Extension>> { const response = await this.#connection.send('Extensions.getExtensions'); const extensionsMap = new Map<string, Extension>(); for (const currExtension of response.extensions) { if (this.#extensions.has(currExtension.id)) { extensionsMap.set( currExtension.id, this.#extensions.get(currExtension.id)!, ); } else { const newExtension = new CdpExtension( currExtension.id, currExtension.version, currExtension.name, currExtension.path, currExtension.enabled, this, this.logger, ); extensionsMap.set(currExtension.id, newExtension); } } this.#extensions = extensionsMap; return this.#extensions; }从源码结构看,该实现包含两层逻辑:
- 协议调用:底层是 CDP 的
Extensions.getExtensions命令,浏览器端一次性返回id / version / name / path / enabled五个字段,正好对应 Extension 的五个属性; - 实例缓存:
CdpBrowser内部维护#extensions缓存 Map。重复调用extensions()时,已见过的扩展 ID 会复用同一个CdpExtension对象,只有新安装的扩展才会新建实例。这意味着在同一浏览器会话中,不同时刻拿到的同一个扩展的Extension实例是稳定的(===相等),可以放心持有引用。
每个新建的实例是 CdpExtension,它在抽象类之外补充了具体行为。以workers()为例,它通过过滤browser.targets()中类型为service_worker且 URL 以chrome-extension://<id>开头的 target 来定位扩展的后端 worker(源码 L36-L64);pages()则过滤page/background_page类型且同前缀 URL 的 target(L66-L94)。值得注意的是,这些方法对已关闭的 target 做了容错——若 worker 或 page 在取值过程中关闭,会忽略target closed类错误并继续返回其余结果,而不是整体抛错。
triggerAction(page)则直接下发 CDP 命令Extensions.triggerAction,携带扩展 ID 与目标页签 ID(L96-L101),效果等同于用户在工具栏点击扩展动作图标。
适用前提与 BiDi 限制
该方法的可用性取决于底层协议。在 BiDi 协议的浏览器实现中,BidiBrowser.extensions() 直接抛出不支持错误:
override extensions(): Promise<Map<string, Extension>> { throw new UnsupportedOperation(); }也就是说:
- Chrome/Chromium 走 CDP 协议(Puppeteer 的默认协议)时,
extensions()可正常使用; - Firefox 或启用 BiDi 协议的场景下,该方法不可用,会抛出
UnsupportedOperation错误; - 使用扩展功能还需要先通过
LaunchOptions.enableExtensions允许扩展加载,或运行时调用browser.installExtension()安装,具体操作参考官方指南 Chrome Extensions。
实战:列出扩展并读取其属性
官方指南 chrome-extensions.md 给出了与本文方法配套的标准用法。先安装扩展,再用extensions()按 ID 取出实例并读取属性:
import puppeteer from 'puppeteer'; import path from 'path'; const pathToExtension = path.join(process.cwd(), 'my-extension'); const browser = await puppeteer.launch({ enableExtensions: true, }); // 运行时安装扩展,返回扩展 ID const extensionId = await browser.installExtension(pathToExtension); // 列出所有已安装扩展,按键取用 const extensions = await browser.extensions(); const extension = extensions.get(extensionId); console.log(extension?.name); // manifest 中的名称 console.log(extension?.version); // manifest 中的版本 console.log(extension?.path); // 扩展在文件系统中的路径 console.log(extension?.enabled); // 是否启用 // 卸载 await browser.uninstallExtension(extensionId);也可以在启动时直接声明加载的扩展路径:
const browser = await puppeteer.launch({ enableExtensions: [pathToExtension], });拿到Extension实例后,还可以进一步与页面交互:await extension.triggerAction(page)触发扩展动作,await extension.workers()获取 service worker 以便在其中执行evaluate,await extension.pages()获取扩展页面。
测试用例中的行为印证
仓库测试 test/src/cdp/extensions.test.ts 中的should list extensions and their properties用例验证了该方法的核心契约:
const extensionId = await browser.installExtension(extensionPath); const target = await browser.waitForTarget(target => { return ( target.url().includes(extensionId) && target.type() === 'service_worker' ); }); const extensions = await browser.extensions(); const extension = extensions.get(extensionId); expect(extension).toBeDefined(); expect(extension?.name).toBe('Simple extension'); expect(extension?.version).toBe('0.1'); expect(extension?.path).toBe(extensionPath); expect(extension?.enabled).toBe(true); expect(extension?.id).toBe(extensionId);该测试确认了三点:Map 以installExtension返回的 ID 为键可命中;name、version取自扩展的 manifest;path、enabled字段分别对应安装路径与启用状态。同文件中的should list extension workers用例(L96-L116)则验证了extensions().get(id)返回的实例可直接调用triggerAction(page)与workers(),形成“列出扩展 → 触发动作 → 获取 worker”的完整闭环。
小结
Browser.extensions()是 Puppeteer 扩展测试体系的查询入口:一次Extensions.getExtensions协议调用即可拿到全部扩展及其元数据,内部缓存保证实例稳定。使用时需记住两个前提——走 CDP 协议、先允许扩展加载(enableExtensions或installExtension);而在 BiDi 协议下该方法会抛出UnsupportedOperation,此时应回退到 CDP 或使用browser.waitForTarget()等 target 级 API 定位扩展的 service worker 与页面。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考