首页
/ 深入理解 Puppeteer `Frame.extensionRealms()`:如何获取页面内扩展内容脚本的执行环境(Realm)

深入理解 Puppeteer `Frame.extensionRealms()`:如何获取页面内扩展内容脚本的执行环境(Realm)

2026-09-06 18:33:31作者:俞予舒Fleming

Frame.extensionRealms() 是 Puppeteer 暴露给扩展自动化场景的一组核心 API:当浏览器扩展的内容脚本(content script)被注入到某个 frame 后,会为该 frame 创建一个独立的执行环境(Realm),而本方法负责把这些环境列举出来,供自动化脚本直接在其中求值。读完本文,你将理解该 API 的签名与返回值、其背后的"扩展世界(extension world)"识别与挂载机制,并能在真实自动化流程中遍历 frame 找到这些 Realm 并与对应的扩展对象建立关联。


什么是 Frame.extensionRealms():官方定义速览

官方 API 文档(docs/api/puppeteer.frame.extensionrealms.md)给出了最直接的定义:

Retrieves the list of extension execution realms associated with this frame. Extension execution realms are created by extension content scripts injected into the frame.

即:该方法返回与当前 frame 关联的"扩展执行环境"列表;这些执行环境由注入该 frame 的扩展内容脚本创建。 在 Puppeteer 的自动化视角下,浏览器扩展注入的脚本并不会污染网页的主执行上下文,而是运行在独立的 Realm 中,而 extensionRealms() 就是程序化触达这些独立上下文的总入口。

方法签名与返回类型

class Frame {
  abstract extensionRealms(): Realm[];
}
项目 说明
所属类 抽象基类 Frame
方法修饰符 abstract,由各协议后端(CDP、BiDi 等)实现
返回值 Realm[],即与当前 frame 关联的所有扩展执行环境
方法用途 拿到扩展内容脚本所在的执行上下文,进而可在其中调用 evaluate / evaluateHandle 等能力

需要说明的是,extensionRealms() 返回的是 Realm 对象数组,而不是扩展对象数组。Realm 是 Puppeteer 中"执行环境"的抽象(参考 puppeteer.realm.md),它承载了在某个独立 JavaScript 上下文中求值的能力。官方声明位于抽象类中(见 packages/puppeteer-core/src/api/Frame.ts#L1231-L1238),说明该能力是跨协议后端设计的。


背景:frame 内的多种执行环境

要理解 extensionRealms() 的价值,需要先清楚一个 frame 内其实同时存在多套 JavaScript 执行上下文。从 packages/puppeteer-core/src/cdp/Frame.ts 的字段设计可以清晰看到这一分层:

  • 主世界(main world):与页面页面脚本共享,代表页面的普通 DOM/JS 上下文;
  • Puppeteer 工具世界(isolated world / PUPPETEER_WORLD:Puppeteer 自身求值所用的隔离上下文;
  • 扩展世界(extension worlds):每一个向该 frame 注入过内容脚本的浏览器扩展,各自拥有独立的执行上下文。

对应到源码,CdpFrame 除了维护主世界与 Puppeteer 世界外,专门声明了:

// packages/puppeteer-core/src/cdp/Frame.ts
extensionWorlds: Record<string, IsolatedWorld> = {};

这是一个以扩展 ID 为键、以 IsolatedWorld 为值的映射表。同一个 frame 可能被多个扩展注入内容脚本,因此 extensionRealms() 返回的是一个数组,而不是单个对象。

概念澄清:仓库源码中"世界(world)"与"Realm"是同义词——文档层的 Realm 抽象对应底层 CDP 的 isolated world 实现。所以扩展的 extensionWorlds 中的每一个 IsolatedWorld,最终都以 Realm 的身份出现在 extensionRealms() 的返回值中。


源码剖析:扩展 Realm 是如何被识别并挂载到 frame 上的

extensionRealms() 的 CDP 实现非常简短,真正复杂的是上层 FrameManager 对执行上下文事件的解析逻辑。

1. CDP 实现:直接返回扩展世界的集合

// packages/puppeteer-core/src/cdp/Frame.ts#L476-L478
override extensionRealms(): Realm[] {
  return Object.values(this.extensionWorlds);
}

CdpFrame 只是把内部按扩展 ID 维护的 extensionWorlds 字典的所有值取出来返回。因此返回值数量 = 当前为该 frame 建立过扩展执行环境的扩展数量,顺序取决于字典插入顺序,与扩展注入顺序一致(从实现细节可以推断)。

2. 事件源头:谁在填充 extensionWorlds

真正把 Realm 创建出来并登记到 frame 上的是 packages/puppeteer-core/src/cdp/FrameManager.ts 中的 #onExecutionContextCreated 处理器。它监听 DevTools 协议的 Runtime.executionContextCreated 事件,并按照以下顺序对一个新出现的执行上下文进行分类(源码对应 #onExecutionContextCreated,约 L607-L652):

  1. 默认上下文:当 auxData.isDefault 为真时,绑定到 frame.worlds[MAIN_WORLD]
  2. Puppeteer 工具上下文:当上下文名等于 UTILITY_WORLD_NAME 时,绑定到 frame.worlds[PUPPETEER_WORLD]
  3. 扩展上下文:当上下文的 originchrome-extension:// 开头时,走扩展分支。

对于扩展分支,FrameManager 依次做了这几件事:

  • #isExtensionOrigin() 判断 origin 是否以扩展协议开头(协议前缀常量定义见 FrameManager.ts#L38const CHROME_EXTENSION_PREFIX = 'chrome-extension://';);
  • #extractExtensionId() 从 origin 中解析出扩展 ID——取 chrome-extension:// 之后直到第一个 / 之前的片段;
  • 按扩展 ID 做去重:如果 frame.extensionWorlds[extId] 已存在则复用,否则 new IsolatedWorld(...) 创建新世界并存入字典;
  • 对新建的扩展世界设置 world.origin = originsetWorldId(extId),并注册相关监听。
// 关键逻辑(节选自 #onExecutionContextCreated,可对照源码查看)
} else if (this.#isExtensionOrigin(origin)) {
  const extId = this.#extractExtensionId(origin);
  if (frame.extensionWorlds[extId]) {
    world = frame.extensionWorlds[extId];
  } else {
    world = new IsolatedWorld(frame, this.timeoutSettings, extId, this.#logger);
    frame.extensionWorlds[extId] = world;
    frame.registerWorldListeners(world);
    world.origin = origin;
    world.setWorldId(extId);
  }
}

从这段实现可以得出一个关键结论:一个扩展 Realm 的产生,取决于浏览器是否真的向该 frame 注入过内容脚本。只有扩展的 manifest 中声明、且匹配当前页面 URL 的内容脚本被激活,Chrome 才会创建对应执行上下文,extensionWorlds 才会被填充。也就是说,extensionRealms() 反映的是运行时真实发生的注入结果

3. 生命周期:frame 被移除时清理扩展世界

CdpFrame[disposeSymbol]() 中(packages/puppeteer-core/src/cdp/Frame.ts#L443-L454),除了销毁主世界与 Puppeteer 世界,还会遍历 extensionWorlds 逐个调用 disposeSymbol() 释放资源。因此 frame 被销毁后,通过该 frame 拿到的 Realm 引用不应再继续使用,这是编写自动化脚本时需要注意的资源生命周期边界。


从 Realm 反向关联扩展:Realm.extension()

拿到 Realm[] 之后,一个自然的问题是:"这些 Realm 分别属于哪个扩展?"答案在配套 API Realm.extension() 中:

Returns the Extension that created this realm, if applicable. This is typically populated when the realm was created by an extension content script injected into a page.

签名与返回:

class Realm {
  abstract extension(): Promise<Extension | null>;
}
  • 返回一个 Promise,resolve 为创建该 Realm 的 Extension 对象;
  • 若该 Realm 并非由扩展创建,则返回 null

Extension 类封装了扩展的元数据与操作入口,其只读属性包括 idnameversionpathenableddocs/api/puppeteer.extension.md),并提供了 pages()(扩展当前可见页面)、workers()(扩展的 service worker)、triggerAction(page)(模拟点击工具栏图标)等方法。若想先枚举浏览器中已安装的全部扩展,官方示例给出了如下写法:

const extensions = await browser.extensions();
for (const [id, extension] of extensions) {
  console.log(extension.name, id);
}

Frame.extensionRealms()Realm.extension() 的配合使用,构成了"从网页 frame 定位到具体扩展执行上下文"的完整链路。


实战:在自动化流程中定位并操作扩展内容脚本上下文

前置条件

扩展自动化是 Puppeteer 的 Chrome 专项能力,需要:

  1. 以支持扩展的方式启动 / 连接到 Chrome(默认下载的 Chrome 构建、持久化用户数据目录等相关细节,见仓库中的 Chrome 扩展自动化指南);
  2. 先将目标扩展安装进浏览器(browser.installextension(...))或保证目标页面本身由扩展提供。

完整示例

下面的脚本演示了"遍历页面所有 frame → 枚举每个 frame 的扩展 Realm → 反向拿到扩展元数据 → 在扩展 Realm 内执行求值"的完整路径:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: false, // 扩展自动化通常需要非 headless 或受支持的 headless 模式
});

const page = await browser.newPage();
await page.goto('https://example.com');

// 等待扩展内容脚本有机会执行
await new Promise(resolve => setTimeout(resolve, 500));

for (const frame of page.frames()) {
  const realms = frame.extensionRealms();

  for (const realm of realms) {
    // 1) 找出该 Realm 属于哪个扩展
    const ext = await realm.extension();
    // 2) 在扩展内容脚本自己的世界里读取 DOM 状态
    const titleHandle = await realm.evaluateHandle(() => {
      return document.title;
    });
    const title = await titleHandle.jsonValue();

    console.log(`[${ext?.name ?? 'unknown'} (${ext?.id})] title=${title}`);
    await titleHandle.dispose();
  }
}

await browser.close();

关键注意点

  • extensionRealms() 可能返回空数组:当页面 URL 不匹配任何已安装扩展的内容脚本匹配规则、扩展内容脚本尚未注入,或扩展被禁用时,frame 内不会产生扩展 Realm,方法返回 []。可在注入前先安装 / 启用扩展并给足脚本注入时间。
  • 每个 frame 独立判断:扩展内容脚本通常可以注入到 iframe 子框架。因此必须像上面的示例一样遍历 page.frames(),而不是只在主 frame 上调用。
  • 求值能力复用 Realm API:返回的每个 Realm 都是完整的执行环境,支持 evaluateevaluateHandlewaitForFunction 等能力(见 docs/api/puppeteer.realm.md),这与对普通页面执行环境的使用方式一致,只不过运行在扩展自己的隔离上下文中,天然规避了与页面脚本的命名冲突。

与其他 API 的关系一览

在 Puppeteer 的扩展自动化版图里,Frame.extensionRealms() 处于"页面侧上下文枚举"这一环:

API 层级 作用
browser.installextension() / browser.extensions() 浏览器级 安装 / 枚举浏览器扩展
Extension.pages() / workers() 扩展级 获取扩展自带的页面与后台 worker
Frame.extensionRealms() frame 级 枚举注入到某 frame 的扩展内容脚本执行环境
Realm.extension() realm 级 反向获取创建该 Realm 的扩展对象
Realm.evaluate() 系列 realm 级 在扩展 Realm 内执行任意代码

从源码结构看,该能力以抽象方法形式定义在 API 层 Frame,并在 CDP 后端 完成实现,Page 类及其 CDP 实现同样引用了 extensionRealms 相关逻辑(见 packages/puppeteer-core/src/api/Page.tspackages/puppeteer-core/src/cdp/Page.ts),可用于从页面级直接汇总所属 frame 的扩展上下文。这种"抽象定义 + 后端实现 + 文档化 API"的组合,也正是 Puppeteer 面向 Chrome/Firefox 双浏览器统一 API 设计的缩影。


小结

Frame.extensionRealms() 表面上只是一个返回 Realm[] 的简单 getter,但它背后是 Puppeteer 对浏览器"多执行环境"模型的完整抽象:FrameManager 通过监听 Runtime.executionContextCreated,用 chrome-extension:// origin 识别扩展上下文、解析扩展 ID、按需创建并缓存 IsolatedWorld,最终由 extensionRealms() 将这批执行环境统一暴露给自动化代码。配合 Realm.extension()Extension 类的方法,你可以实现"在页面中自动定位扩展内容脚本 → 识别归属扩展 → 在扩展隔离上下文中执行代码"的完整链路,为 Chrome 扩展的端到端测试与自动化操作提供了标准入口。

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