Puppeteer Browser.browserContexts() 详解:枚举、跟踪与管理所有浏览器上下文的官方 API
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
本文围绕 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.1BrowserContext.closed:用"是否在列表里"判定上下文是否已关闭
BrowserContext基类的closed属性直接通过browserContexts()反向查询实现(api/BrowserContext.ts):
get closed(): boolean { return !this.browser().browserContexts().includes(this); }也就是说,一个上下文是否关闭,等价于它是否还出现在browserContexts()的返回列表中。上下文被close()后从内部容器移除,closed随即变为true。这解释了为什么该方法必须是同步且实时读取注册表的。
4.2Browser.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.3Browser.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 中安全地编排任意数量的隔离会话。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考