深入理解 Puppeteer `Frame.extensionRealms()`:如何获取页面内扩展内容脚本的执行环境(Realm)
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):
- 默认上下文:当
auxData.isDefault为真时,绑定到frame.worlds[MAIN_WORLD]; - Puppeteer 工具上下文:当上下文名等于
UTILITY_WORLD_NAME时,绑定到frame.worlds[PUPPETEER_WORLD]; - 扩展上下文:当上下文的
origin以chrome-extension://开头时,走扩展分支。
对于扩展分支,FrameManager 依次做了这几件事:
- 用
#isExtensionOrigin()判断 origin 是否以扩展协议开头(协议前缀常量定义见 FrameManager.ts#L38:const CHROME_EXTENSION_PREFIX = 'chrome-extension://';); - 用
#extractExtensionId()从 origin 中解析出扩展 ID——取chrome-extension://之后直到第一个/之前的片段; - 按扩展 ID 做去重:如果
frame.extensionWorlds[extId]已存在则复用,否则new IsolatedWorld(...)创建新世界并存入字典; - 对新建的扩展世界设置
world.origin = origin、setWorldId(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 类封装了扩展的元数据与操作入口,其只读属性包括 id、name、version、path、enabled(docs/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 专项能力,需要:
- 以支持扩展的方式启动 / 连接到 Chrome(默认下载的 Chrome 构建、持久化用户数据目录等相关细节,见仓库中的 Chrome 扩展自动化指南);
- 先将目标扩展安装进浏览器(
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 都是完整的执行环境,支持
evaluate、evaluateHandle、waitForFunction等能力(见 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.ts 与 packages/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 扩展的端到端测试与自动化操作提供了标准入口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00