Puppeteer Browser.browserContexts() 详解:枚举、跟踪与管理所有浏览器上下文的官方 API
本文围绕 Puppeteer 官方 API 文档 docs/api/puppeteer.browser.browsercontexts.md 中定义的 Browser.browserContexts() 方法展开,讲清它的签名语义、在 CDP 与 BiDi 两种协议下的具体实现,以及它在 BrowserContext.closed 判定、Browser.pages() 聚合等内部调用链中的核心作用。读完本文,你既能正确地在自动化脚本中枚举和管理多个浏览器上下文(isolated context / incognito),也能从源码层面理解"新浏览器只有一个上下文"这一行为背后的实现机制。
一、方法签名与基本语义
Browser.browserContexts() 定义在抽象基类 Browser 上,用于获取当前浏览器实例中所有打开的浏览器上下文列表。官方文档(docs/api/puppeteer.browser.browsercontexts.md)给出的完整签名如下:
class Browser {
abstract browserContexts(): BrowserContext[];
}
返回值:BrowserContext[](BrowserContext 数组)
关键语义有三点:
- 同步方法,无参数:它不返回
Promise,读取的是 Puppeteer 侧内存中维护的上下文注册表,因此调用开销极低,可以在任意时机(包括事件回调中)调用。 - 新浏览器只有 1 个上下文:文档明确指出 "In a newly-created browser, this will return a single instance of BrowserContext"。这唯一的实例就是默认浏览器上下文(default browser context),可通过
browser.defaultBrowserContext()单独获取。 - 上下文意味着存储隔离:每个
BrowserContext拥有相互隔离的 cookies、localStorage 等存储。在 Chrome 中,所有非默认上下文都是 incognito(无痕)模式;默认上下文是否无痕取决于启动时是否传入--incognito参数(见 docs/api/puppeteer.browsercontext.md 的 Remarks 部分)。
抽象声明位于 api/Browser.ts:
/**
* 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[];
二、上下文从哪里来:createBrowserContext() 与默认上下文
browserContexts() 返回的列表,其元素来源只有两条路径:
- 默认上下文:浏览器启动时由 Puppeteer 自动创建,不能被关闭;
- 显式创建:调用
browser.createBrowserContext(options?)创建的新上下文,签名与用法见 docs/api/puppeteer.browser.createbrowsercontext.md:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
// 创建一个全新的浏览器上下文(与其他上下文不共享 cookies/cache)
const context = await browser.createBrowserContext();
// 在该上下文中创建页面
const page = await context.newPage();
await page.goto('https://example.com');
官方示例(同时收录在 api/Browser.ts 的 JSDoc 中)演示了最典型的"干净上下文"用法:新上下文不继承任何既有登录态与缓存,天然适合做隔离测试或多账号场景。
一个完整的上下文生命周期示例:
// 创建新浏览器上下文
const context = await browser.createBrowserContext();
// 在上下文中创建页面
const page = await context.newPage();
await page.goto('https://example.com');
// ... 使用 page ...
// 上下文不再需要时销毁
await context.close();
createBrowserContext 与 browserContexts() 在源码中是"写"与"读"的对应关系:前者把新上下文注册进内部容器,后者负责枚举该容器。理解这一点对后面阅读两种协议实现至关重要。
三、源码级实现:CDP 与 BiDi 两套引擎
Puppeteer 同时支持 CDP(Chrome DevTools Protocol)与 WebDriver BiDi 两种协议,browserContexts() 各有独立实现。
3.1 CDP 实现:默认上下文 + 上下文 Map
CDP 版的 CdpBrowser 用一个 Map<browserContextId, CdpBrowserContext> 保存所有非默认上下文,默认上下文则单独持有。browserContexts() 就是把默认上下文拼在 Map 值之前:
override browserContexts(): CdpBrowserContext[] {
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
}
见 cdp/Browser.ts。从源码结构看:
- 返回值顺序是确定的:默认上下文永远排在数组首位;
- 新浏览器只有一个上下文的解释:
#contextsMap 初始为空,数组里就只有#defaultContext一项; createBrowserContext()的写入路径:该实现先向浏览器发送Target.createBrowserContext协议命令拿到browserContextId,再new CdpBrowserContext(...)并存入#contexts(见 cdp/Browser.ts);上下文关闭时由_disposeContext()发送Target.disposeBrowserContext并从 Map 中删除(cdp/Browser.ts)。因此browserContexts()的列表长度随上下文的创建/销毁实时增减。
3.2 BiDi 实现:基于 UserContext 的 WeakMap
BiDi 版的 BidiBrowser 使用 WeakMap<UserContext, BidiBrowserContext> 把底层协议的 user context 映射到 Puppeteer 的 BrowserContext 对象:
override browserContexts(): BidiBrowserContext[] {
return [...this.#browserCore.userContexts].map(context => {
return this.#browserContexts.get(context)!;
});
}
见 bidi/Browser.ts。浏览器初始化时会遍历 browserCore.userContexts 逐个建立映射(#initialize(),bidi/Browser.ts),因此 browserContexts() 返回的是当前协议侧全部现存 user context 的映射结果,默认上下文同样包含在内(由 defaultBrowserContext() 依据 browserCore.defaultUserContext 定位)。
3.3 上下文标识 id 的取值差异
与 browserContexts() 配合使用时常会读取 context.id。两个实现的取值规则不同(从源码结构看):
- CDP:非默认上下文返回协议返回的
browserContextId,见 cdp/BrowserContext.ts; - BiDi:默认 user context 返回
undefined,其余返回userContext.id,见 bidi/BrowserContext.ts。
基类 BrowserContext.id 默认返回 undefined(api/BrowserContext.ts)。这意味着跨协议编写脚本时,不宜把"id 一定存在"当作前提。
四、内部调用链:browserContexts() 支撑哪些 API
browserContexts() 虽然看起来只是一个"取列表"的方法,但它实际上是 Puppeteer 多处聚合逻辑与状态判定的基础。
4.1 BrowserContext.closed:用"是否在列表里"判定上下文是否已关闭
BrowserContext 基类的 closed 属性直接通过 browserContexts() 反向查询实现(api/BrowserContext.ts):
get closed(): boolean {
return !this.browser().browserContexts().includes(this);
}
也就是说,一个上下文是否关闭,等价于它是否还出现在 browserContexts() 的返回列表中。上下文被 close() 后从内部容器移除,closed 随即变为 true。这解释了为什么该方法必须是同步且实时读取注册表的。
4.2 Browser.pages():跨上下文聚合所有页面
Browser 基类实现的 pages() 会遍历所有上下文并合并各自的结果(api/Browser.ts):
async pages(includeAll = false): Promise<Page[]> {
const contextPages = await Promise.all(
this.browserContexts().map(context => {
return context.pages(includeAll);
}),
);
// Flatten array.
return contextPages.reduce((acc, x) => {
return acc.concat(x);
}, []);
}
因此 browser.pages() 与逐个上下文调用 context.pages(includeAll?) 的差异正是"是否跨上下文聚合"。注意:不可见的页面(如 "background_page")不会被列出,这类页面可通过 Target.page() 找到(见 docs/api/puppeteer.browsercontext.pages.md 的 Remarks)。
4.3 Browser.targets():BiDi 版同样基于上下文聚合
BiDi 实现的 targets() 也是先枚举上下文再平铺其 targets()(bidi/Browser.ts),与文档中"存在多个上下文时返回所有上下文中的全部 targets"(api/Browser.ts)的描述一致。
五、测试用例中的行为验证
仓库集成测试 test/src/browsercontext.test.ts 对 browserContexts() 的行为做了系统验证,可作为该 API 契约的直接依据:
- 新浏览器至少有一个上下文:
expect(browser.browserContexts().length).toBeGreaterThanOrEqual(1)(test/src/browsercontext.test.ts); - 创建/关闭使列表长度增减:创建上下文后
expect(browser.browserContexts()).toHaveLength(contextCount + 1),且indexOf(context) !== -1为true;close()后长度恢复(test/src/browsercontext.test.ts); - 新创建的上下文不共享存储:测试创建两个 incognito 上下文,各自
targets()为空、cookies 互不可见(test/src/browsercontext.test.ts); - 跨会话一致:通过
puppeteer.connect({ browserWSEndpoint })重新连接同一浏览器后,remoteBrowser.browserContexts()仍能正确列出已创建的上下文(test/src/browsercontext.test.ts)——这说明上下文的注册状态来自浏览器端协议状态,而非仅存在于单个 Puppeteer 连接内。
此外,测试还验证了默认上下文不可关闭(defaultContext.close() 会抛错,test/src/browsercontext.test.ts)以及新上下文会带有 id(test/src/browsercontext.test.ts)。
六、实战模式:用 browserContexts() 管理上下文生命周期
以下模式均以 browserContexts() 为观察入口,适用于日常自动化与测试框架开发。
6.1 多账号并行会话
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const accounts = ['alice@example.com', 'bob@example.com'];
for (const email of accounts) {
const context = await browser.createBrowserContext(); // 独立 cookies/存储
const page = await context.newPage();
// ... 在该 page 中完成 email 对应的登录流程 ...
await page.goto('https://example.com/login?user=' + encodeURIComponent(email));
}
console.log(browser.browserContexts().length); // 3:默认 + 2 个账号上下文
每个账号会话处于独立上下文,登录态互不污染;关闭某个上下文即彻底清理该账号的会话数据。
6.2 周期性对账:清理"僵尸"上下文
在长驻进程(如浏览器农场、CI 中复用的浏览器)中,可以周期性检查上下文数量是否与预期任务一致:
function reconcileContexts(browser: Awaited<ReturnType<typeof puppeteer.launch>>, expected: number) {
const contexts = browser.browserContexts();
if (contexts.length > expected) {
// 关闭多余的上下文(注意:默认上下文 close() 会抛错,应先排除)
const defaultContext = browser.defaultBrowserContext();
return Promise.all(
contexts.filter(c => c !== defaultContext && c.closed !== true).map(c => c.close()),
);
}
return Promise.resolve();
}
6.3 重连后恢复状态
Puppeteer.connect 重连已有浏览器时,browserContexts() 是恢复管理视图的第一步:
const browser = await puppeteer.connect({ browserWSEndpoint });
const contexts = browser.browserContexts();
for (const context of contexts) {
const pages = await context.pages();
console.log(context.id ?? '(default)', pages.length);
}
这与 docs/api/puppeteer.connect.md 描述的连接流程配合使用;测试用例 "should work across sessions" 已验证重连后列表的完整性。
6.4 结合 BrowserContextEvent 事件流
browserContexts() 提供的是快照,配合 BrowserContextEvent(targetcreated / targetchanged / targetdestroyed)可构建完整的上下文/目标生命周期监控。例如在上下文内 window.open 产生新 target 时监听 TargetCreated,再结合 context.waitForTarget() 等待特定 URL 的 target 出现(示例见 api/BrowserContext.ts 中 waitForTarget 的 JSDoc)。
七、注意事项与边界
- 默认上下文不可关闭:
defaultBrowserContext()返回的实例调用close()会抛错;批量关闭上下文前先与默认上下文区分(依据 docs/api/puppeteer.browsercontext.close.md 的 Remarks 与上述测试用例)。 - 返回值是同步快照,不是订阅:它不监听后续变化;列表在两次调用之间可能变化,需要持续跟踪请结合事件(
BrowserContextEvent、BrowserEvent)。 - incognito 语义仅限 Chrome 文档明确描述:"在 Chrome 中所有非默认上下文都是 incognito";BiDi 下上下文对应协议的 user context,其行为以浏览器实现为准,编写跨协议脚本时避免对无痕特性做硬假设。
id可能为undefined:基类默认返回undefined,BiDi 下默认上下文返回undefined(见第 3.3 节),以id作为键存入 Map 时需做兜底。- 页面列表的可见性过滤:基于
browserContexts()聚合的pages()不含"background_page"等不可见页面,如需完整目标请使用targets()+Target.page()。
小结
Browser.browserContexts() 是 Puppeteer 多上下文能力的枚举入口:签名简单(同步、无参、返回 BrowserContext[]),但它是上下文注册表的权威读取接口——BrowserContext.closed 的状态判定、Browser.pages() / Browser.targets() 的跨上下文聚合、重连后的状态恢复,全部构建在它之上。CDP 实现以"默认上下文 + Map<id, context>"组织(cdp/Browser.ts),BiDi 实现以 WeakMap<UserContext, BidiBrowserContext> 组织(bidi/Browser.ts),两者共同保证了"新浏览器恰有一个默认上下文、createBrowserContext() 增加一项、close() 移除一项"的一致行为,这一契约由 test/src/browsercontext.test.ts 中的计数断言直接固化。掌握该方法后,配合 createBrowserContext()、context.newPage() 与 context.close(),即可在 Puppeteer 中安全地编排任意数量的隔离会话。
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 StartedRust0627
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