Puppeteer Browser.newPage() 深度解析:创建新标签页、独立窗口与后台页面的完整指南
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
本篇围绕 Puppeteer 的Browser.newPage()方法展开:它是获取Page实例最直接的入口,在默认浏览器上下文(default browser context)中创建一个新页面。读完本文,你不仅能掌握该方法的完整签名、CreatePageOptions各参数(type: 'tab' | 'window'、windowBounds、background)的用法,还能从源码层面理解它在 CDP 与 BiDi 两种协议下分别如何落到Target.createTarget/browsingContext.create调用,以及如何配合Page.windowId()、Browser.getWindowBounds()验证窗口行为,并通过仓库内测试用例确认各项能力的实际表现。
方法签名与基本行为
按照官方 API 文档(docs/api/puppeteer.browser.newpage.md),该方法在默认浏览器上下文中创建一个新页面:
class Browser { abstract newPage(options?: CreatePageOptions): Promise<Page>; }| 参数 | 类型 | 说明 |
|---|---|---|
options | CreatePageOptions | 可选,控制页面创建方式(标签页/独立窗口)与是否后台运行 |
返回值:Promise<Page>,解析为创建好的 Page 实例。
Browser抽象类中的定义位于 packages/puppeteer-core/src/api/Browser.ts,其 JSDoc 明确写着 "Creates a new page in the default browser context",并给出了最小可用示例:
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await browser.close();需要注意一个隐含前提:newPage()创建的是默认上下文里的页面。默认上下文共享 Cookie、缓存与权限状态,且不能被关闭(见 Browser.defaultBrowserContext 文档:"The default browser context cannot be closed.")。如果你需要隔离的环境,应改用browser.createBrowserContext()后再调用context.newPage(),两者的options类型相同,实现见 BrowserContext.newPage 文档。
CreatePageOptions 参数详解
CreatePageOptions是Browser与BrowserContext共用的选项类型,在源码中的定义见 packages/puppeteer-core/src/api/Browser.ts#L255-L270(类型文档:docs/api/puppeteer.createpageoptions.md):
export type CreatePageOptions = ( | { type?: 'tab'; } | { type: 'window'; windowBounds?: WindowBounds; } ) & { /** * Whether to create the page in the background. * @defaultValue `false` */ background?: boolean; };这是一个联合类型与{ background?: boolean }的交叉,实际含义如下:
type: 'tab'(默认)
不传options或传{type: 'tab'}时,行为一致:在当前浏览器窗口的标签栏中新开一个标签页。这是最常见、也是性能开销最小的用法——共享同一窗口、同一 GPU 进程与渲染资源,适合绝大多数爬取、截图、多任务并行的场景。
type: 'window'+windowBounds
传{type: 'window'}会强制新页面在独立窗口中打开,并可通过windowBounds指定窗口的几何位置、尺寸与窗口状态。WindowBounds接口定义在 packages/puppeteer-core/src/api/Browser.ts#L236-L245,所有字段均为可选(接口文档:docs/api/puppeteer.windowbounds.md):
| 字段 | 类型 | 含义 |
|---|---|---|
left | number | 窗口左上角的 X 坐标 |
top | number | 窗口左上角的 Y 坐标 |
width | number | 窗口宽度 |
height | number | 窗口高度 |
windowState | WindowState | 窗口状态 |
WindowState的取值为(源码 api/Browser.ts#L231-L234):
export type WindowState = 'normal' | 'minimized' | 'maximized' | 'fullscreen';典型用法——在指定位置打开一个 750×550 的独立窗口:
const page = await browser.newPage({ type: 'window', windowBounds: {left: 50, top: 50, width: 750, height: 550}, });或者只指定窗口状态:
// 以最大化状态打开新窗口 const page = await browser.newPage({ type: 'window', windowBounds: {windowState: 'maximized'}, });background
background与type正交,可以叠加使用。设为true时新页面以后台方式创建,页面不会获得焦点,document.visibilityState为'hidden',适合在后台静默运行任务(如预热页面、批量抓取),避免抢占用户当前焦点。
const page = await browser.newPage({background: true}); // page.evaluate(() => document.visibilityState) => 'hidden'源码实现:CDP 协议下的调用链
newPage()在 CDP 协议下的实现位于CdpBrowser,其核心是一条清晰的委托链:
CdpBrowser.newPage()转发到默认上下文(packages/puppeteer-core/src/cdp/Browser.ts#L410-L412):
override async newPage(options?: CreatePageOptions): Promise<Page> { return await this.#defaultContext.newPage(options); }CdpBrowserContext.newPage()加锁后调用_createPageInContext(packages/puppeteer-core/src/cdp/BrowserContext.ts#L132-L135):
override async newPage(options?: CreatePageOptions): Promise<Page> { using _guard = await this.waitForScreenshotOperations(); return await this.#browser._createPageInContext(this.#id, options); }这里的#id对默认上下文为undefined,因此browser.newPage()与browser.defaultBrowserContext().newPage()最终走的是同一条路径。waitForScreenshotOperations()则保证创建页面前该上下文上已挂起的截屏操作完成,避免并发冲突。
_createPageInContext()是真正发协议的地方(packages/puppeteer-core/src/cdp/Browser.ts#L414-L455):
const hasTargets = this.targets().filter(t => { return t.browserContext().id === contextId; }).length > 0; const windowBounds = options?.type === 'window' ? options.windowBounds : undefined; const {targetId} = await this.#connection.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined, left: windowBounds?.left, top: windowBounds?.top, width: windowBounds?.width, height: windowBounds?.height, windowState: windowBounds?.windowState, // Works around crbug.com/454825274. newWindow: hasTargets && options?.type === 'window' ? true : undefined, background: options?.background, });从源码结构看,可以提取出几个关键实现事实:
- 新页面初始 URL 固定为
about:blank,后续由你自己goto或setContent; windowBounds只有在type === 'window'时才会生效——源码中windowBounds变量的赋值就带了这个条件判断,若你传了type: 'tab'同时带了windowBounds,它会被静默忽略(类型系统上也不会允许,因为'tab'分支没有windowBounds字段);newWindow参数是一个 Chromium bug 的规避:仅当上下文中已存在其他 target 且请求新建窗口时才置true,源码注释标注 "Works around crbug.com/454825274";background直接透传给Target.createTarget,由浏览器侧处理后台标签的可见性状态。
拿到targetId后,实现并不立即返回,而是用browser.waitForTarget(t => t._targetId === targetId)等待对应的Target对象被 TargetManager 注册,再检查target._initializedDeferred是否为SUCCESS(初始化失败会抛出Failed to create target for page (id = ...)),最后通过target.page()取出包装好的Page对象返回。也就是说,newPage()resolve 时,页面 target 已经完成初始化,你可以直接在上面执行goto、evaluate等操作。
BiDi 协议下的实现差异
Puppeteer 同时支持 WebDriver BiDi 协议(相关背景见 docs/webdriver-bidi.md),其实现位于 packages/puppeteer-core/src/bidi/BrowserContext.ts#L206-L242:
const type = options?.type === 'window' ? Bidi.BrowsingContext.CreateType.Window : Bidi.BrowsingContext.CreateType.Tab; const context = await this.userContext.createBrowsingContext(type, { background: options?.background, }); ... if (options?.type === 'window' && options?.windowBounds !== undefined) { try { await this.browser().setWindowBounds( context.windowId, options.windowBounds, ); } catch (error) { // Tolerate not supporting `browser.setClientWindowState`. Only log it. this.#logger?.(DEBUG_PREFIXES.error)?.(error); } }与 CDP 实现的差异值得注意:
type映射到 BiDi 的BrowsingContext.CreateType(Window/Tab),background同样透传;windowBounds采用"先创建、后调整"的两步策略:先创建窗口,再调用browser.setWindowBounds(context.windowId, windowBounds)调整。这与 CDP 中把left/top/width/height/windowState一次性塞进Target.createTarget的做法不同;- 若浏览器不支持
browser.setClientWindowState能力,异常会被捕获并只记录日志而不中断创建流程("Tolerate not supporting"),即 BiDi 下windowBounds属于"尽力而为"; - 若 launch 时配置了
defaultViewport,BiDi 实现还会对新页面补一次page.setViewport(this.#defaultViewport)。
这意味着在 BiDi 模式下,windowBounds相关能力对具体浏览器实现存在兼容性前提,使用建议以 CDP(Chrome)为主。
实战:创建、验证与窗口控制
通过 windowId 验证窗口几何
创建独立窗口后,可用Page.windowId()拿到所属窗口 ID,再用Browser.getWindowBounds()验证几何是否生效(windowId()实现见 packages/puppeteer-core/src/cdp/Page.ts#L439-L445,内部调用Browser.getWindowForTarget;getWindowBounds/setWindowBounds实现见 cdp/Browser.ts#L642-L657):
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); const initialBounds = {left: 10, top: 20, width: 800, height: 600}; const page = await browser.newPage({ type: 'window', windowBounds: initialBounds, }); const windowId = await page.windowId(); const actual = await browser.getWindowBounds(windowId); console.log(actual); // {left: 10, top: 20, width: 800, height: 600, ...} // 后续还可动态调整窗口位置/尺寸 await browser.setWindowBounds(windowId, {left: 200, top: 200, width: 1024, height: 768}); await page.close(); await browser.close();仓库测试 test/src/browser.test.ts 中的用例正是这个模式:创建带windowBounds的窗口页后断言getWindowBounds返回的对象与初始值一致;另一个用例先通过browser.addScreen()添加第二块屏幕,再把新窗口开到副屏上(left: screenInfo.availLeft + 50, ...),演示了newPage的多屏窗口布局能力。
后台页面与可见性
test/src/page.test.ts 的 "should create a background page" 用例验证了background: true的语义:
const page = await context.newPage({background: true}); expect( await page.evaluate(() => { return document.visibilityState; }), ).toBe('hidden');即后台页面的document.visibilityState为'hidden'——如果你依赖requestAnimationFrame或IntersectionObserver的可见性触发逻辑,后台页面中这些 API 的行为需要留意。
多窗口并发示例
const browser = await puppeteer.launch({headless: false}); // 标签页 1:共享窗口 const tab1 = await browser.newPage(); // 独立窗口 2:固定位置与尺寸 const win2 = await browser.newPage({ type: 'window', windowBounds: {left: 400, top: 100, width: 1200, height: 800}, }); // 后台窗口 3:不抢焦点 const bg = await browser.newPage({type: 'window', background: true}); const pages = await browser.pages(); console.log(pages.length); // 3browser.pages()会聚合所有上下文中的页面(api/Browser.ts#L637-L647),可用于确认新建页面已登记;注意非可见页面(如background_page类型的 target)不会出现在列表中。
行为验证:仓库测试用例覆盖
以下测试用例均可在仓库中直接找到,为上述行为提供了验证依据:
- 新窗口包含检查(test/src/page.test.ts#L38-L48):
context.newPage({type: 'window'})后,context.pages()与browser.pages()都应包含该页面; - 指定位置与尺寸(test/src/page.test.ts#L49-L66):
windowBounds: {left: 50, top: 50, width: 750, height: 550}后,通过window.outerWidth/outerHeight断言外框尺寸精确为 750×550,证明 bounds 作用于窗口外框而非视口; - 最大化状态(test/src/page.test.ts#L67-L85):
windowState: 'maximized'下窗口外框尺寸等于 headless 默认屏幕的 800×600; getWindowBounds回读(test/src/browser.test.ts):创建带 bounds 的窗口后,browser.getWindowBounds(page.windowId())与传入值toMatchObject相等。
与 BrowserContext.newPage() 的关系及 API 索引
browser.newPage(options)本质上等价于browser.defaultBrowserContext().newPage(options)——从 CDP 实现看二者汇聚到同一个_createPageInContext(contextId, options),仅contextId不同(默认上下文为undefined)。两者的取舍:
| 场景 | 推荐入口 |
|---|---|
| 简单脚本、所有页面共享状态 | browser.newPage() |
| 需要隔离 Cookie/缓存/权限(多用户、多站点) | browser.createBrowserContext()→context.newPage() |
| 创建纯净环境后释放 | 非默认上下文支持context.close(),默认上下文不可关闭 |
相关 API 文档索引(均以仓库根目录为基准):
- CreatePageOptions 类型
- WindowBounds 接口
- Page 类
- Browser.defaultBrowserContext
- BrowserContext.newPage
- Browser.getWindowBounds / Browser.setWindowBounds
- Page.windowId
小结
Browser.newPage()虽只有一个可选参数,但背后覆盖了三类创建语义:默认标签页(type: 'tab')、可精确控制位置/尺寸/窗口状态的独立窗口(type: 'window'+windowBounds)、以及不抢焦点的后台页面(background: true)。CDP 实现通过一次Target.createTarget(含newWindow的 bug workaround)加waitForTarget初始化等待完成创建;BiDi 实现则采用"创建后 setWindowBounds"的两步策略并容忍能力缺失。日常使用记住三点即可:初始 URL 恒为about:blank;windowBounds仅对type: 'window'生效且作用于窗口外框;隔离需求请走BrowserContext.newPage()而非叠加默认上下文页面。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考