Puppeteer 的 BrowserContext.pages():列出隔离浏览上下文中的所有 Page 对象
在 Puppeteer 中,BrowserContext.pages() 用于获取某个隔离浏览上下文(browser context)内当前所有打开的 Page 实例。它回答了一个自动化场景中的高频问题:"这个隔离环境里现在到底有哪些标签页?"——例如做多账号并行采集时需要遍历各自独立的上下文,或在关闭上下文前批量清理页面。本文基于 Puppeteer 官方 API 文档 BrowserContext.pages() 条目,结合 packages/puppeteer-core/src/api/BrowserContext.ts 及 CDP/BiDi 两套实现的源码,讲清该方法的签名、参数语义、过滤规则与底层调用链。
API 签名与参数说明
官方文档给出的方法签名为:
class BrowserContext {
abstract pages(includeAll?: boolean): Promise<Page[]>;
}
对应源码定义位于 packages/puppeteer-core/src/api/BrowserContext.ts:
/**
* Gets a list of all open {@link Page | pages} inside this
* {@link BrowserContext | browser context}.
*
* @param includeAll - experimental, setting to true includes all kinds of pages.
*
* @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
* will not be listed here. You can find them using {@link Target.page}.
*/
abstract pages(includeAll?: boolean): Promise<Page[]>;
| 参数 | 类型 | 说明 |
|---|---|---|
includeAll |
boolean(可选) |
实验性参数。设置为 true 时纳入更多"页面类"目标 |
返回值:Promise<Page[]>,解析为该上下文内所有已打开页面的数组。
两个关键注意事项(文档 Remarks 与源码注释一致):
- 默认返回的是"可见页面"。像
"background_page"这类非可见页面不会出现在结果中; - 需要查找这类隐藏页面时,应改用
Target.page()(即先拿到目标,再主动尝试取页面),而不是依赖pages()的默认过滤。
典型用法
BrowserContext 通常通过 browser.createBrowserContext() 创建,pages() 常与其配合使用来盘点上下文内的页面:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
// 创建一个隔离的浏览器上下文(在 Chrome 中等价于一次隐身会话)
const context = await browser.createBrowserContext();
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto('https://example.com/');
await page2.goto('https://example.org/');
// 列出该上下文内所有页面
const pages = await context.pages();
console.log(pages.length); // 2
console.log(pages.map(p => p.url()));
// 任务结束后关闭上下文会连带关闭其中的所有页面
await context.close();
与 pages() 形成对照的还有 Browser.pages():它会遍历 browser.browserContexts(),对每个上下文分别调用 context.pages(includeAll) 后拍平合并,返回所有上下文内的页面:
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() 的全局聚合版本。仓库测试 test/src/browsercontext.test.ts 也验证了这一行为:在主上下文新建页面后,browser.pages() 与 context.pages() 的长度分别按预期增长;再新建一个独立上下文时,旧上下文的 pages() 只统计自己的页面。
底层实现:CDP 通道如何过滤页面
pages() 在抽象基类中只是声明,真正逻辑在传输层实现里。CDP 通道的实现位于 packages/puppeteer-core/src/cdp/BrowserContext.ts:
override async pages(includeAll = false): Promise<Page[]> {
const pages = await Promise.all(
this.targets()
.filter(target => {
return (
target.type() === 'page' ||
((target.type() === 'other' || includeAll) &&
this.#browser._getIsPageTargetCallback()?.(target))
);
})
.map(target => {
return target.page();
}),
);
return pages.filter(page => {
return !!page;
});
}
调用链可以拆成三步:
- 取目标集合:
this.targets()先过滤出属于当前上下文的目标。同一文件的targets()实现就是this.#browser.targets().filter(target => target.browserContext() === this),即按"目标归属的 browser context 是否为本实例"做隔离——这正是pages()结果不会串到其他上下文的原因; - 按类型过滤:保留
target.type() === 'page'的目标;对于类型为'other'(或开启includeAll的其他类型)的目标,则委托给浏览器级的"是否页面目标"回调做二次判断; - 转换为 Page 对象:对幸存目标逐个调用
target.page(),最后用pages.filter(page => !!page)剔除无法解析为Page的目标(target.page()对非页面目标可能返回undefined)。
那个二次判断回调的默认实现在 packages/puppeteer-core/src/cdp/Browser.ts:
#setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void {
this.#isPageTargetCallback =
isPageTargetCallback ||
((target: Target): boolean => {
return (
target.type() === 'page' ||
target.type() === 'background_page' ||
target.type() === 'webview' ||
(this.#handleDevToolsAsPage &&
target.type() === 'other' &&
isDevToolsPageTarget(target.url()))
);
});
}
从源码结构看,默认回调覆盖四类目标:page、background_page、webview,以及在 handleDevToolsAsPage 开启时 URL 判定为 DevTools 的 other 目标。这解释了文档 Remarks 的措辞:background_page 等类型能进入候选集,但 target.page() 最终能否解析出 Page 实例、以及默认过滤条件的组合,决定了 pages() 实际返回的集合;要显式定位这类隐藏页面,文档给出的正路仍是通过 Target 对象调用 Target.page()。
BiDi 通道的实现差异
在 WebDriver BiDi 通道下,packages/puppeteer-core/src/bidi/BrowserContext.ts 的实现完全不同:
override async pages(_includeAll = false): Promise<BidiPage[]> {
return [...this.userContext.browsingContexts].map(context => {
return this.#pages.get(context)!;
});
}
可以直接观察到一个差异:BiDi 实现把 includeAll 参数下划线化(_includeAll),即当前实现中该参数并不参与过滤逻辑,而是直接枚举底层 userContext.browsingContexts 中注册的 browsing context,并从 #pages 映射表取对应 BidiPage。这与 CDP 实现按目标类型过滤的思路不同——从源码结构看,BiDi 侧的"页面"概念以 browsing context 注册表为准,includeAll 目前仅是保持签名一致的占位。使用时应注意:includeAll 被文档标注为实验性参数,其行为在不同通道之间可能存在差异,生产代码不宜对其做强依赖。
小结
BrowserContext.pages(includeAll?)返回当前隔离上下文内所有已打开页面的Promise<Page[]>,是 BrowserContext 上盘点页面的标准入口;- 默认结果不含
"background_page"等不可见页面,需要时用 Target.page() 主动查找(见 BrowserContext.pages() 文档); - CDP 实现通过
targets()归属过滤 + 目标类型过滤 +target.page()解析三步得到结果,includeAll会放宽类型过滤条件; browser.pages()是对所有上下文context.pages(includeAll)结果的拍平聚合,适合跨上下文的整体巡检。
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 StartedRust0622
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