Puppeteer Browser.pages() API 详解:枚举浏览器中所有打开页面的正确姿势
Browser.pages() 是 Puppeteer 浏览器实例上的一个基础但关键的 API:它一次性返回当前浏览器中所有打开的 Page 对象。本篇基于 API 文档 展开,结合 packages/puppeteer-core 的源码实现,讲清楚它的签名、参数、跨浏览器上下文(Browser Context)的行为、includeAll 的过滤语义,以及 CDP 与 WebDriver BiDi 两套协议实现下的差异,帮助你在自动化脚本和测试中可靠地枚举、遍历并管理页面。
API 签名与参数说明
Browser.pages() 的官方签名如下:
class Browser {
pages(includeAll?: boolean): Promise<Page[]>;
}
行为要点(与 文档 一致):
- 返回该
Browser内部所有打开的 Page 列表; - 如果浏览器中存在多个浏览器上下文(BrowserContext),则返回所有上下文中的页面,而不是仅限默认上下文。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
includeAll |
boolean |
(可选)实验性参数。设为 true 时会包含各种类型的页面(见下文过滤逻辑解析)。 |
返回值
Promise<Page[]> —— 一个包含所有 Page 实例的数组。
重要备注(Remarks)
不可见的页面(non-visible pages),例如 "background_page" 类型的页面,默认不会出现在返回值中。这类页面可以通过 Target.page() 来查找。这一点是理解该 API 行为边界的关键,下面会结合源码解释为什么。
跨上下文聚合: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);
}, []);
}
调用链非常清晰:
- 通过
this.browserContexts()拿到当前浏览器下的全部浏览器上下文(包括默认上下文和通过createBrowserContext()创建的隔离上下文); - 对每个上下文并发调用
context.pages(includeAll),将includeAll参数原样透传; - 用
Promise.all等待所有结果后,用reduce将二维数组展平为一维Page[]。
也就是说,Browser.pages() 本身不直接与底层协议交互,真正的"哪些目标(Target)算页面"的判断下沉到了各协议实现(CDP 或 BiDi)的 BrowserContext.pages() 中。对应的抽象声明见 api/BrowserContext.ts:abstract pages(includeAll?: boolean): Promise<Page[]>。
CDP 实现:includeAll 到底过滤了什么
在 CDP 协议下,BrowserContext.pages() 的实现位于 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;
});
}
拆解这段过滤逻辑:
- 第一步:取该上下文内的所有
targets()(通过target.browserContext() === this过滤,见 L55-L59); - 第二步:一个目标被纳入,当且仅当
- 它的
target.type()就是'page'(普通可见页面,默认即被包含);或者 - 目标类型是
'other',或者includeAll为true(实验性参数会放宽类型门槛),并且通过isPageTarget回调的判断;
- 它的
- 第三步:对命中的目标调用
target.page()转换为Page对象; - 第四步:过滤掉
null(即某些目标无法创建出Page实例的情况),保证返回数组中只有可用的Page。
默认的"页面目标"判定回调
isPageTarget 回调的默认定义在 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 时对 'other' 类型的 DevTools 页面。
把两段代码合起来看,就能得到文档 Remarks 的完整解释:
- 默认(
includeAll = false):只有target.type() === 'page'的目标直接通过过滤;'background_page'等目标不满足该条件,因此不会出现在Browser.pages()的结果中; includeAll = true:类型门槛放宽,'background_page'、'webview'等目标会经过isPageTarget回调判断并可能被包含进来(这也是该参数被标注为"experimental"的原因——它会改变返回集合的组成,且不同浏览器版本的目标类型集合可能存在差异)。
如果需要拿到 background_page 这类非可见页面,更稳妥的方式是直接遍历 browser.targets(),对每个 Target 调用 Target.page() 尝试获取 Page 实例,而不是依赖 includeAll。
BiDi 实现:includeAll 当前被忽略
除了 CDP,Puppeteer 还支持 WebDriver BiDi 协议。其上下文实现位于 bidi/BrowserContext.ts:
override async pages(_includeAll = false): Promise<BidiPage[]> {
return [...this.userContext.browsingContexts].map(context => {
return this.#pages.get(context)!;
});
}
与 CDP 实现相比有两个差异值得注意:
- 它直接遍历 BiDi 的
userContext.browsingContexts(用户上下文中的浏览上下文列表),再从内部映射表this.#pages取出对应的BidiPage对象; - 参数名以下划线开头(
_includeAll),意味着 BiDi 路径当前不使用includeAll参数。因此,如果你的脚本依赖includeAll = true的语义,请确认自己运行在 CDP 协议(protocol: 'cdp')下,这是该参数的适用前提。
实战用法
1. 枚举并遍历所有页面
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
// 打开几个页面(含隔离上下文)
const context = await browser.createBrowserContext();
const pageA = await browser.newPage(); // 默认上下文
const pageB = await context.newPage(); // 隔离上下文
await pageA.goto('https://example.com/');
await pageB.setContent('<h1>isolated</h1>');
// 一次性拿到所有上下文中的全部页面
const allPages = await browser.pages();
for (const page of allPages) {
console.log(page.url(), await page.title());
}
2. 关闭全部页面而保持浏览器连接
这是 Browser.pages() 最典型的清理场景。Puppeteer 自身的测试 test/src/browser.test.ts 验证了"关闭所有页面后浏览器仍保持连接、且可继续 newPage()"的行为:
it('should keep connected after the last page is closed', async () => {
const {browser, close} = await launch({});
try {
const pages = await browser.pages();
await Promise.all(
pages.map(page => {
return page.close();
}),
);
// Verify the browser is still connected.
expect(browser.connected).toBe(true);
// Verify the browser can open a new page.
await browser.newPage();
} finally {
await close();
}
});
对应到日常脚本中,即可实现"会话级清理":
const pages = await browser.pages();
await Promise.all(pages.map(page => page.close()));
// 此时 browser 仍处于 connected 状态,可继续 newPage(),最后再 browser.close()
3. 只清理某个上下文内的页面
由于 Browser.pages() 返回的是跨上下文聚合的结果,如果你只想清理某个隔离上下文,应使用粒度更细的 BrowserContext.pages():
const context = await browser.createBrowserContext();
// ... 使用 context ...
const contextPages = await context.pages();
await Promise.all(contextPages.map(p => p.close()));
await context.close(); // 默认上下文不能 close,隔离上下文可以
这一模式在测试 test/src/browsercontext.test.ts 中也有广泛使用,可参考其上下文隔离的断言方式。
小结与参考
Browser.pages()的语义是"跨全部浏览器上下文的可见页面(type === 'page')列表",底层实现在 api/Browser.ts,逐上下文聚合后展平;- 页面筛选发生在各协议实现层:CDP 路径见 cdp/BrowserContext.ts,
includeAll(实验性)会放宽目标类型门槛,配合默认isPageTarget回调(cdp/Browser.ts)可纳入background_page、webview等目标;BiDi 路径见 bidi/BrowserContext.ts,当前不消费includeAll; background_page等非可见页面默认不列出,需要时用 Target.page() 从targets()中查找;- 相关 API 文档:Browser、BrowserContext.pages()、Page、Target;
- 相关测试:test/src/browser.test.ts、test/src/browsercontext.test.ts、test/src/target.test.ts。
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