Puppeteer Browser.browserContexts() 方法详解:枚举浏览器上下文的 API 与 CDP/BiDi 实现剖析
本文以 Puppeteer 官方 API 文档中的 Browser.browserContexts() 方法为主体,完整讲解该方法的签名、返回值语义、新建浏览器时的默认行为,并结合 puppeteer-core 源码剖析 CDP 与 WebDriver BiDi 两套协议下的具体实现,以及测试套件对其行为的验证方式。读完后你将能够正确地在自动化脚本中枚举、统计和管理一个 Browser 实例下的所有 BrowserContext,并理解默认上下文与自建上下文在内部存储结构上的差异。
一、方法定义与返回值语义
browserContexts() 是 Browser 类上用于获取所有已打开浏览器上下文列表的方法。官方 API 文档(docs/api/puppeteer.browser.browsercontexts.md)给出的方法说明为:
Gets a list of open browser contexts.(获取已打开浏览器上下文的列表。)
In a newly-created browser, this will return a single instance of BrowserContext.(在一个新建的浏览器中,该方法将只返回一个
BrowserContext实例。)
方法签名为:
class Browser {
abstract browserContexts(): BrowserContext[];
}
返回值: BrowserContext[] —— 一个同步返回的数组,包含当前浏览器实例下所有未关闭的 BrowserContext。
几个关键语义点:
- 同步方法:与
createBrowserContext()(返回Promise<BrowserContext>)不同,browserContexts()是同步方法,直接读取内存中维护的上下文注册表,不发起任何协议请求。 - 新建浏览器只含默认上下文:浏览器刚启动时,数组中只有一个元素,即默认浏览器上下文(default browser context)。
- 每次
createBrowserContext()都会使数组长度 +1,每次context.close()后又会恢复原长度(详见后文测试验证)。 - 返回的列表中默认上下文位于首位——这一实现细节在 CDP 实现中可以清晰看到。
二、BrowserContext 概念回顾
理解 browserContexts() 的前提是理解 BrowserContext 本身。根据 BrowserContext 类文档:
BrowserContext表示浏览器内的独立用户上下文。浏览器启动时至少拥有一个默认上下文,其余可通过Browser.createBrowserContext()创建;- 每个上下文拥有隔离的存储(cookies / localStorage 等),互不共享;
- 在 Chrome 中,所有非默认上下文都是 incognito(隐私)上下文;如果启动参数中提供了
--incognito,默认上下文也可能处于 incognito 状态; - 该类的构造函数被标记为内部(internal),第三方代码不应直接实例化或继承
BrowserContext; - 页面通过
window.open打开的弹出窗口会归属于父页面所在的浏览器上下文。
典型用法(来自官方文档示例):
// Create a new browser context
const context = await browser.createBrowserContext();
// Create a new page inside context.
const page = await context.newPage();
// ... do stuff with page ...
await page.goto('https://example.com');
// Dispose context once it's no longer needed.
await context.close();
browserContexts() 正是用来观察这一整套"创建—使用—销毁"生命周期的观测入口。
三、源码剖析:抽象声明与两套协议实现
3.1 抽象基类中的声明
在 packages/puppeteer-core/src/api/Browser.ts 中,browserContexts() 被声明为抽象方法:
/**
* Gets a list of open {@link BrowserContext | browser contexts}.
*
* In a newly-created {@link Browser | browser}, this will return a single
* instance of {@link BrowserContext}.
*/
abstract browserContexts(): BrowserContext[];
/**
* Gets the default {@link BrowserContext | browser context}.
*
* @remarks The default {@link BrowserContext | browser context} cannot be
* closed.
*/
abstract defaultBrowserContext(): BrowserContext;
注意它与 defaultBrowserContext() 的分工:browserContexts() 返回全部上下文(含默认上下文),而 defaultBrowserContext() 只返回默认上下文,且默认上下文不可被关闭。
3.2 CDP 实现:默认上下文 + Map 注册表
Chrome 走 CDP 协议,对应实现在 packages/puppeteer-core/src/cdp/Browser.ts:
override browserContexts(): CdpBrowserContext[] {
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
}
override defaultBrowserContext(): CdpBrowserContext {
return this.#defaultContext;
}
其中 #defaultContext 与 #contexts(Map<string, CdpBrowserContext>,以 browserContextId 为键)在 构造函数中初始化:
this.#defaultContext = new CdpBrowserContext(
this.#connection,
this,
undefined, // 默认上下文没有 contextId
logger,
);
for (const contextId of contextIds) {
this.#contexts.set(
contextId,
new CdpBrowserContext(this.#connection, this, contextId, logger),
);
}
从源码结构看,CDP 实现有三个要点:
- 返回顺序固定:默认上下文始终排在数组第一位,其后是按插入顺序排列的自建上下文;
- 默认上下文无
contextId:构造时第三个参数传undefined,这也是BrowserContext.id(string | undefined)允许为undefined的原因——自建上下文会携带由 CDP 协议分配的browserContextId; - 连接既有浏览器时可携带初始上下文:
contextIds参数用于puppeteer.connect()场景,把远端浏览器上已经存在的上下文批量注册进#contexts,从而保证browserContexts()返回的列表是完整的(测试用例"should work across sessions"验证了这一点,见第四节)。
自建上下文的创建路径为 createBrowserContext():向浏览器发送 Target.createBrowserContext CDP 命令拿到 browserContextId,再构造 CdpBrowserContext 并写入 #contexts,因此创建成功后立即调用 browserContexts() 就能看到新上下文。
3.3 BiDi 实现:userContexts 映射
Firefox(以及走 WebDriver BiDi 协议的 Chrome)使用 packages/puppeteer-core/src/bidi/Browser.ts 中的实现:
override browserContexts(): BidiBrowserContext[] {
return [...this.#browserCore.userContexts].map(context => {
return this.#browserContexts.get(context)!;
});
}
override defaultBrowserContext(): BidiBrowserContext {
return this.#browserContexts.get(this.#browserCore.defaultUserContext)!;
}
可以推断,BiDi 实现维护一个 #browserCore(协议侧浏览器核心),其 userContexts 属性提供所有用户上下文 ID,#browserContexts 是一个 ID 到 BidiBrowserContext 实例的缓存表;browserContexts() 把两者做映射后返回。默认上下文则通过 #browserCore.defaultUserContext 单独定位。BiDi 核心层还会通过 browsingContext.getTree 同步上下文树(见 packages/puppeteer-core/src/bidi/core/Browser.ts),在 getTree 期间若上下文被创建或销毁,会借助 browsingContext.contextCreated 事件检测,保证上下文列表的实时性。
两套实现虽然内部数据结构不同,但对外行为一致:都返回含默认上下文在内的全部上下文列表,均满足"新建浏览器返回单个默认上下文"的文档承诺。
3.4 一个隐藏细节:closed 判定依赖 browserContexts()
browserContexts() 不只是查询接口,它还参与 BrowserContext 的关闭状态判定。在抽象基类 packages/puppeteer-core/src/api/BrowserContext.ts 中,closed 属性的判定逻辑正是通过成员检测实现的:
return !this.browser().browserContexts().includes(this);
即"不在 browserContexts() 返回列表中的上下文即为已关闭"。这说明上下文的关闭必须保证它从注册表中被移除,browserContexts() 因此是上下文生命周期状态的权威来源。
四、测试套件验证的行为契约
test/src/browsercontext.test.ts 针对 browserContexts() 的行为写了多组断言,可作为该方法的"可验证契约":
契约 1:默认浏览器至少有一个上下文(第 17-23 行):
it('should have default context', async () => {
const {browser} = await getTestState({skipContextCreation: true});
expect(browser.browserContexts().length).toBeGreaterThanOrEqual(1);
});
契约 2:创建/关闭上下文会精确改变列表长度与成员(第 38-50 行):
const contextCount = browser.browserContexts().length;
expect(contextCount).toBeGreaterThanOrEqual(1);
const context = await browser.createBrowserContext();
expect(browser.browserContexts()).toHaveLength(contextCount + 1);
expect(browser.browserContexts().indexOf(context) !== -1).toBe(true);
await context.close();
expect(browser.browserContexts()).toHaveLength(contextCount);
契约 3:跨连接会话共享同一份上下文列表(第 218-236 行):
expect(browser.browserContexts()).toHaveLength(1);
const context = await browser.createBrowserContext();
try {
expect(browser.browserContexts()).toHaveLength(2);
using remoteBrowser = await puppeteer.connect({
browserWSEndpoint: browser.wsEndpoint(),
protocol: browser.protocol,
});
const contexts = remoteBrowser.browserContexts();
expect(contexts).toHaveLength(2);
} finally {
await context.close();
}
最后这个用例特别有价值:它证明 puppeteer.connect() 建立的新 Browser 实例(即第二节的 contextIds 注册路径)能枚举出与本地实例完全一致的上下文数量,因此 browserContexts() 在多进程/分布式控制场景下同样可靠。
契约 4:自建上下文携带 id(第 238-251 行):createBrowserContext() 之后 context.id 被断言为已定义(toBeDefined()),与 BrowserContext.id: string | undefined 的类型声明(见 BrowserContext 文档 的属性表)互相印证。
五、实战用法
5.1 统计与枚举浏览器中的所有上下文
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
// 新建浏览器:仅默认上下文
console.log(browser.browserContexts().length); // 1
// 创建两个隔离上下文(例如:匿名会话 + 已登录会话)
const anonymous = await browser.createBrowserContext();
const logged = await browser.createBrowserContext();
for (const context of browser.browserContexts()) {
console.log('context id:', context.id); // 默认上下文为 undefined,自建上下文为字符串
console.log('pages:', context.pages().length);
}
// 用完即关:列表长度恢复为 1
await anonymous.close();
await logged.close();
await browser.close();
注意默认上下文的 id 为 undefined,如需识别默认上下文,应使用 browser.defaultBrowserContext() 做引用比较,而不是依赖 id。
5.2 多账号并行采集的隔离模型
browserContexts() 常与 createBrowserContext() 配合,实现"一个浏览器进程、多个隔离会话"的架构:每个账号分配一个独立上下文,各自的 cookies / localStorage 互不可见。定期调用 browserContexts() 可以核对实际存活上下文数量是否符合预期(防止上下文泄漏——例如任务结束忘记 close())。
六、与相关 API 的关系
| API | 文档 | 与 browserContexts() 的关系 |
|---|---|---|
Browser.createBrowserContext(options) |
docs/api/puppeteer.browser.createbrowsercontext.md | 创建新上下文,会使列表长度 +1;支持 proxyServer、proxyBypassList、downloadBehavior 等 BrowserContextOptions |
Browser.defaultBrowserContext() |
docs/api/puppeteer.browser.defaultbrowsercontext.md | 只取默认上下文,不遍历列表;默认上下文不可关闭 |
BrowserContext.close() |
docs/api/puppeteer.browsercontext.close.md | 关闭上下文及其全部页面,关闭后该上下文从 browserContexts() 中消失 |
BrowserContext.pages(includeAll) |
docs/api/puppeteer.browsercontext.pages.md | 获取单个上下文内页面;Browser.pages() 则是对其遍历所有上下文后取并集(见 api/Browser.ts 中对 browserContexts() 的 flatMap 式调用) |
BrowserContext.targets() |
docs/api/puppeteer.browsercontext.targets.md | 获取单个上下文内活跃 target |
七、结论与参考路径
browserContexts() 是 Puppeteer 中管理多上下文(多隔离会话)的观测与核对入口:同步返回、含默认上下文、CDP 实现下默认上下文恒居首位。其正确性由 puppeteer-core 的 CDP/BiDi 双实现保证,并由 test/src/browsercontext.test.ts 的长度变化、成员包含、跨会话一致性三组断言固化。
本文涉及的关键仓库路径:
- 文档:docs/api/puppeteer.browser.browsercontexts.md、docs/api/puppeteer.browsercontext.md、docs/api/puppeteer.browser.md
- 抽象声明:packages/puppeteer-core/src/api/Browser.ts
- CDP 实现:packages/puppeteer-core/src/cdp/Browser.ts、创建路径 createBrowserContext
- BiDi 实现:packages/puppeteer-core/src/bidi/Browser.ts、核心同步 packages/puppeteer-core/src/bidi/core/Browser.ts
- 测试:test/src/browsercontext.test.ts
适用前提:以上行号与实现细节基于当前仓库版本(puppeteer-core 源码,browserContexts() 为同步方法的版本);在不同 Puppeteer 大版本间,BiDi 上下文同步等内部实现可能调整,公共 API 语义以官方文档为准。
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 StartedRust0629
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