首页
/ Puppeteer ExtensionInstallOptions 接口深度解析:在 Chrome 隐身模式(Incognito)下启用扩展的运行时配置

Puppeteer ExtensionInstallOptions 接口深度解析:在 Chrome 隐身模式(Incognito)下启用扩展的运行时配置

2026-09-06 18:14:08作者:傅爽业Veleda

ExtensionInstallOptions 是 Puppeteer 为 Browser.installExtension() 运行时安装扩展提供的唯一选项接口,用于控制扩展是否在 Chrome 的隐身(Incognito,即 Off-The-Record/OTR)配置文件中启用。本文基于当前仓库中的 接口 API 文档Browser.installExtension() 文档,结合 接口源码、CDP 实现、浏览器启动器代码与真实测试用例,完整讲解该接口的属性语义、使用方式、底层调用链与跨浏览器差异。读完你将掌握如何在 Puppeteer 测试脚本中安装一个"隐身模式下同样可用"的 Chrome 扩展,并理解它与启动期 enableExtensions 等配置项之间的关系。

接口总览:ExtensionInstallOptions

该接口在当前仓库的 API 文档中定义如下(文档页为 puppeteer.extensioninstalloptions.md):

export interface ExtensionInstallOptions

接口本身不继承任何父类型,只声明一个属性,其完整签名对应 Browser.ts 中的公开源码

export interface ExtensionInstallOptions {
  /**
   * Whether to enable the extension in Incognito or OTR profiles in Chrome.
   */
  enabledInIncognito: boolean;
}

它是 Browser.installExtension() 的可选第二参数据类型。参考 Browser.installExtension() 的方法文档,抽象类 Browser 的声明如下:

class Browser {
  abstract installExtension(
    path: string,
    options?: ExtensionInstallOptions,
  ): Promise<string>;
}

也就是说:installExtension(path) 只需一个指向扩展源码目录的路径即可完成安装,options 是可选参数;而当需要影响"该扩展在隐身配置文件中是否可用"时,才需要传入 ExtensionInstallOptions

enabledInIncognito 属性详解

下表完整列出了接口唯一的属性(与文档表格一一对应):

Property Type Description Default
enabledInIncognito boolean Whether to enable the extension in Incognito or OTR profiles in Chrome.(是否在 Chrome 的隐身 / Off-The-Record 配置文件中启用该扩展) 省略时按 false 处理

对语义的进一步解读:

  • 作用对象是"配置文件"而非页面:Chrome 中"隐身窗口"运行于独立的 Incognito/OTR profile。普通 profile 安装的扩展默认不会在隐身模式下加载,只有显式开启"允许在隐身模式下运行"才会生效。该属性的目的就是等价地控制这一点。
  • 与 Puppeteer 的隐身上下文对应:Puppeteer 中通过 browser.createBrowserContext() 创建的新上下文,正是与扩展的 Incognito 能力相对应的隔离环境,详见下文"测试验证"。
  • 省略时的行为:属性本身是必填字段(enabledInIncognito: boolean,无 ?),但作为整体 options 参数是可选的。看 CDP 侧实现 可以确认:当不传该选项时,实现层用 ?? false 兜底,即默认在隐身模式下启用:
override async installExtension(
  path: string,
  options?: ExtensionInstallOptions,
): Promise<string> {
  const {id} = await this.#connection.send('Extensions.loadUnpacked', {
    path,
    enableInIncognito: options?.enabledInIncognito ?? false,
  });
  this.#extensions.delete(id);
  return id;
}

核心使用场景:运行时安装"隐身可用"的扩展

使用 ExtensionInstallOptions 的典型前提是:浏览器是以允许扩展的模式启动的。运行时安装配合隐身着色的完整流程可参考官方指南 Chrome Extensions 中的做法,写法如下:

import puppeteer from 'puppeteer';
import path from 'path';

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

// 1. 启动时允许扩展:必须为 true,否则默认启动参数会禁用扩展
const browser = await puppeteer.launch({
  enableExtensions: true,
});

// 2. 运行时安装扩展,并允许其在 Incognito / OTR profile 中运行
const extensionId = await browser.installExtension(pathToExtension, {
  enabledInIncognito: true,
});

// 3. 新建隐身上下文(Incognito)并打开页面
const context = await browser.createBrowserContext();
const page = await context.newPage();
await page.goto('https://example.com');

// 4. 扩展相关目标(例如 MV3 service worker)应已出现在隐身上下文中
await browser.waitForTarget(
  target =>
    target.type() === 'service_worker' &&
    target.url().includes(extensionId),
);

// 5. 若需验证内容脚本 realm,可遍历 page.extensionRealms() 并调用其 evaluate()
const realms = page.extensionRealms();
let extensionRealm;
for (const realm of realms) {
  const extension = await realm.extension();
  if (extension?.id === extensionId) {
    extensionRealm = realm;
    break;
  }
}
if (!extensionRealm) {
  throw new Error('Extension realm not found');
}
const result = await extensionRealm.evaluate(() => {
  return document.title;
});

// 6. 清理:卸载扩展并关闭隐身上下文
await browser.uninstallExtension(extensionId);
await context.close();
await browser.close();

要点说明:

  • installExtension 的返回值是被安装扩展的 ID(string),后续通过 browser.extensions() 拿到 Map<string, Extension> 或以该 ID 匹配 target URL 时都需要它。
  • enabledInIncognito: true 只影响"扩展能否进入隐身 profile",并不会自动创建任何上下文——你仍需要用 browser.createBrowserContext() 主动开启隐身环境。
  • 卸载使用 browser.uninstallExtension(id),参见 uninstallExtension 源码

底层原理:选项如何映射到 CDP 命令

ExtensionInstallOptions 不是 Puppeteer 本地逻辑,而是直接对应 Chrome DevTools Protocol 中 Extensions.loadUnpacked 命令的 enableInIncognito 入参。从源码可以看出命名存在一处微妙差异,值得注意:

  • 接口层使用 enabledInIncognito(形容词化命名,描述"是否已启用"的状态语义);
  • CDP 入参使用 enableInIncognito(动词短语,描述"启用"这个动作)。

两者通过 cdp/Browser.ts 的一行发送调用完成桥接:

const {id} = await this.#connection.send('Extensions.loadUnpacked', {
  path,
  enableInIncognito: options?.enabledInIncognito ?? false,
});

因此在阅读该扩展的文档或调试 CDP 流量时,需要同时在两种命名间做对应,避免因拼写差异误认为字段不一致。

启动期加载与 enabledInIncognito 的关系

除了运行时手动调用 installExtension,Puppeteer 还支持在 launch() 时一次性加载扩展。这两条路径最终共享同一套 ExtensionInstallOptions 机制,具体可见两个启动选项的定义 LaunchOptions.ts

/**
 * If `true`, avoids passing default arguments to the browser that would
 * prevent extensions from being enabled. Passing a list of strings will
 * load the provided paths as unpacked extensions.
 */
enableExtensions?: boolean | string[];

/**
 * List of extensions that will be enable in Incognito and off-the-record
 * profiles.
 */
extensionsEnabledInIncognito?: string[];

enableExtensions 传入路径数组时,启动器会逐个调用 installExtension,并把"该路径是否出现在 extensionsEnabledInIncognito 列表里"转化为 enabledInIncognito 选项。见 BrowserLauncher.ts

if (Array.isArray(enableExtensions)) {
  await Promise.all([
    enableExtensions.map(path => {
      return browser.installExtension(path, {
        enabledInIncognito: extensionsEnabledInIncognito.includes(path),
      });
    }),
  ]);
}

也就是说,以下两种写法等价:

// 写法一:启动期配置,依赖 extensionsEnabledInIncognito 隐式构造选项
const browser = await puppeteer.launch({
  enableExtensions: [pathToExtension],
  extensionsEnabledInIncognito: [pathToExtension],
});
// 写法二:启动期仅开扩展能力,安装动作延后到运行时并显式传选项
const browser = await puppeteer.launch({
  enableExtensions: true,
});
const extensionId = await browser.installExtension(pathToExtension, {
  enabledInIncognito: true,
});

从源码结构可以推断,写法二更适合需要在"决定是否开启隐身能力"之前先执行其他逻辑(例如先获取扩展 ID、根据扩展清单内容做条件判断)的测试场景。

测试验证:仓库中的真实用例

当前仓库为这一选项提供了直接的回归测试,位于 test/src/cdp/extensions.test.ts,用例名为 "should be available in Incognito profiles if enabledInIncognito is true",其验证链路可拆解为四步:

  1. {enabledInIncognito: true} 安装扩展,拿到 extensionId
  2. browser.createBrowserContext() 创建隐身上下文,并导航到空页面;
  3. 断言隐身上下文中出现了 URL 包含该扩展 ID 的 service_worker 目标,证明扩展确实被加载进隐身 profile;
  4. 遍历 page.extensionRealms(),找到与 extensionId 匹配的扩展 realm,并 evaluate 读取内容脚本注入的全局变量 thisIsTheContentScript 以确认真实可用。

该测试同时验证了两个事实:enabledInIncognito: true 确实能让扩展进入隐身上下文;而卸载(uninstallExtension)与关闭上下文后,测试末尾的 assertNoServiceWorkerReported 会确认相关 service worker 目标不再被 Puppeteer 报告。若将选项改为省略或 false,上述第 3、4 步在隐身上下文中将无法命中,这正是该选项存在意义的最直接反证。

跨浏览器差异:为什么选项只在 Chrome 生效

ExtensionInstallOptions 的属性描述本身注明了适用范围是 Chrome。当前仓库同时支持基于 WebDriver BiDi 的 Firefox 后端,对比两份实现可以清楚地看到差异:

  • Chrome/CDP 后端完整接收并消费该选项(见上文 cdp/Browser.ts);
  • Firefox/BiDi 后端的 installExtension 只接收路径参数,直接透传给浏览器核心,不接收任何选项对象:bidi/Browser.ts
override installExtension(path: string): Promise<string> {
  return this.#browserCore.installExtension(path);
}

因此可以得出如下结论,供选用浏览器时参考:

  • enabledInIncognito 属于 Chrome 特性,是编写仅针对 Chrome 的扩展端到端测试时才有意义的配置;
  • 若在 Firefox(BiDi)后端上为 installExtension 传入该选项,从当前实现看它不会参与任何处理逻辑;
  • 在编写跨浏览器扩展测试时,应将"隐身可用"相关断言限定在 Chrome 专属用例中,避免对两种后端给出相同的期望。

注意事项与最佳实践

综合文档、源码与测试,实践中有几点建议:

  1. 别忘了启动开关:默认的 Puppeteer 启动参数会阻止扩展运行。无论走 enableExtensions: true 还是 enableExtensions: [路径],都必须显式开启,否则 installExtension 即使成功,扩展目标也不会出现。扩展相关的完整操作方式可继续参考 Chrome Extensions 指南
  2. 默认即"不启用":省略 options 与显式传 {enabledInIncognito: false} 效果一致,均不会让扩展进入隐身上下文。
  3. 用返回值做后续关联installExtension 返回的字符串 ID 是查询 browser.extensions()extension.pages() 与等待目标的统一钥匙,建议一拿到就保存复用。
  4. 注意清理顺序:仓库测试中的惯例是先 uninstallExtensioncontext.close(),避免卸载动作触发的事件与隐身上下文生命周期互相干扰。
  5. 命名差异防踩坑:接口属性 enabledInIncognito 与 CDP 字段 enableInIncognito 差一个字母,手工抓包或阅读协议层代码时需按上文给出的映射对照。

综上,ExtensionInstallOptions 虽只有一个属性,却贯穿了运行时安装、启动期加载、CDP 协议桥接与隐身上下文测试这条完整链路,是 Puppeteer 扩展测试中"让扩展在隐身模式下可用"这一需求的最小而关键的配置入口。

登录后查看全文
热门项目推荐
相关项目推荐