Puppeteer Browser.addScreen 详解:在 headless 模式下动态扩展虚拟屏幕
本文基于 Puppeteer 官方 API 文档 docs/api/puppeteer.browser.addscreen.md,详解 Browser.addScreen() 方法的签名、AddScreenParams 全部参数、返回值 ScreenInfo 的字段含义,并结合仓库源码还原其 CDP(Chrome DevTools Protocol)底层调用链,给出可在 headless 模式下直接复制运行的多屏模拟示例(含配合 windowBounds 将窗口放到副屏、最大化验证、以及 removeScreen 清理的完整测试用例)。
一、方法定位与签名
addScreen() 是 Browser 类上的抽象方法,作用是向当前受控浏览器新增一块虚拟屏幕,并返回所添加屏幕的信息对象。其 TypeScript 签名为:
class Browser {
abstract addScreen(params: AddScreenParams): Promise<ScreenInfo>;
}
- 参数:
params,类型为 AddScreenParams,描述新屏幕的几何位置、尺寸、色彩深度等属性; - 返回值:
Promise<ScreenInfo>,即 ScreenInfo 接口对象,包含该屏幕的完整信息(包括系统生成的id、isPrimary、availWidth等); - 重要限制(官方 Remarks 原文):Only supported in headless mode——仅支持无头模式。在 headed(有头)模式下调用没有意义,因为屏幕由操作系统真实提供;多屏能力只有在 headless 环境中才是由 Chromium 自身模拟出来的。
在 api/Browser.ts 中,addScreen 与 screens()、removeScreen() 构成同一组屏幕管理 API:
// packages/puppeteer-core/src/api/Browser.ts
/**
* Adds a new screen, returns the added screen information object.
*
* @remarks
* Only supported in headless mode.
*/
abstract addScreen(params: AddScreenParams): Promise<ScreenInfo>;
/**
* Removes a screen.
*
* @remarks
* Only supported in headless mode. Fails if the primary screen id is specified.
*/
abstract removeScreen(screenId: string): Promise<void>;
由此可以确认两点:新增屏幕后可用 screens() 枚举全部屏幕;删除屏幕用 removeScreen(screenId),且主屏 id 不允许删除。
二、AddScreenParams 参数逐项说明
AddScreenParams 接口定义于 api/Browser.ts(接口文档见 AddScreenParams):
/**
* @public
*/
export interface AddScreenParams {
left: number;
top: number;
width: number;
height: number;
workAreaInsets?: WorkAreaInsets;
devicePixelRatio?: number;
rotation?: number;
colorDepth?: number;
label?: string;
isInternal?: boolean;
}
/**
* @public
*/
export interface WorkAreaInsets {
top?: number;
left?: number;
bottom?: number;
right?: number;
}
各字段含义与约束如下(必填/可选与源码修饰符严格一致):
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
left |
number | 必填 | 新屏幕左上角相对虚拟桌面原点的水平偏移(像素)。典型用法是紧接主屏右侧,如主屏宽 800 时传 left: 800 |
top |
number | 必填 | 新屏幕左上角的垂直偏移(像素),通常副屏传 0 |
width |
number | 必填 | 屏幕宽度(像素) |
height |
number | 必填 | 屏幕高度(像素) |
workAreaInsets |
WorkAreaInsets | 可选 | 工作区四边内缩,模拟任务栏/ dock 等遮挡区域。四边 top/left/bottom/right 均为可选 number。设置后 availWidth/availHeight 会相应减小 |
devicePixelRatio |
number | 可选 | 设备像素比(DPR),用于模拟高 DPI 屏幕;未设置时 Chromium 默认返回 1 |
rotation |
number | 可选 | 屏幕旋转角度,用于模拟竖屏/横屏方向 |
colorDepth |
number | 可选 | 色深,如 24、32,对应屏幕返回的 colorDepth |
label |
string | 可选 | 屏幕标签名,用于在 screens() 结果中区分屏幕,如 'secondary' |
isInternal |
boolean | 可选 | 是否内部屏幕,对应返回对象的 isInternal 字段 |
从源码结构看,workAreaInsets 的实际效果在测试断言中得到印证:屏幕高 1200 且 workAreaInsets: {bottom: 80} 时,返回的 availHeight 为 1120(1200 − 80),即“可用高度”已扣除底部 80 像素的任务栏区域。
三、返回值 ScreenInfo 字段
addScreen() 返回的 ScreenInfo 对象描述新屏幕的最终状态,关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 屏幕唯一标识,后续 removeScreen(screenId) 必须使用它 |
left / top |
number | 屏幕左上角坐标,与入参一致 |
width / height |
number | 屏幕宽高,与入参一致 |
availLeft / availTop / availWidth / availHeight |
number | 扣除 workAreaInsets 后的工作区坐标与尺寸,是把窗口精确放进该屏幕时的可靠依据 |
devicePixelRatio |
number | 设备像素比,未指定时默认 1 |
colorDepth |
number | 色深,默认 24 |
isPrimary / isInternal / isExtended |
boolean | 主屏/内部屏/扩展屏标志;addScreen 新增的屏幕恒为 isPrimary: false、isExtended: true |
label |
string | 入参中的屏幕标签 |
orientation |
ScreenOrientation | 屏幕方向,包含 angle 与 type(如 {angle: 0, type: 'landscapePrimary'}),见 ScreenOrientation |
注意:ScreenInfo 中的 id、isPrimary、isExtended、orientation、avail* 字段均为 Chromium 自动生成,AddScreenParams 并不直接接收——入参负责“怎么放这块屏”,返回对象负责“这块屏最终的完整描述”。
四、底层实现:CDP 调用链
在 CDP 实现中,addScreen 只是一次对浏览器级 DevTools 会话的 Emulation.addScreen 命令转发,实现在 cdp/Browser.ts:
override async screens(): Promise<ScreenInfo[]> {
const {screenInfos} = await this.#connection.send(
'Emulation.getScreenInfos',
);
return screenInfos;
}
override async addScreen(params: AddScreenParams): Promise<ScreenInfo> {
const {screenInfo} = await this.#connection.send(
'Emulation.addScreen',
params,
);
return screenInfo;
}
override async removeScreen(screenId: string): Promise<void> {
return await this.#connection.send('Emulation.removeScreen', {screenId});
}
可以确认:
AddScreenParams原样作为Emulation.addScreen的参数发送给浏览器进程,Puppeteer 层不做数值换算;- 返回值为 CDP 响应中的
screenInfo字段,直接作为ScreenInfo透出; - 屏幕增删查三个方法对应 CDP 命令一一对应:
Emulation.addScreen/Emulation.removeScreen/Emulation.getScreenInfos。
另外,该能力仅在 CDP(Chrome DevTools Protocol)连接下可用。在 BiDi 实现的 bidi/Browser.ts 中,addScreen(连同 screens、removeScreen)直接抛出 UnsupportedOperation:
override screens(): Promise<ScreenInfo[]> {
throw new UnsupportedOperation();
}
override addScreen(_params: AddScreenParams): Promise<ScreenInfo> {
throw new UnsupportedOperation();
}
因此使用前提可归纳为:Chrome + CDP 连接 + headless 模式。
五、实战示例:新增副屏、放置窗口与清理
以下示例直接取自仓库测试套件 test/src/browser.test.ts 的 Browser.add|removeScreen 用例,展示了完整的“添加 → 校验 → 删除”闭环:
// 1. 在主屏(800x600)右侧新增一块 1600x1200 的副屏,底部预留 80px 任务栏
const screenInfo = await browser.addScreen({
left: 800, // 紧贴主屏右边缘
top: 0,
width: 1600,
height: 1200,
colorDepth: 32,
workAreaInsets: {bottom: 80},
label: 'secondary',
});
// 2. 校验返回对象:availHeight = 1200 - 80 = 1120,isExtended 为 true
// 注意 id 由浏览器生成,用 screenInfo.id 记录以便后续删除
expect(screenInfo.isExtended).toBe(true);
expect(screenInfo.isPrimary).toBe(false);
expect(screenInfo.availHeight).toBe(1120);
// 3. screens() 应返回两块屏幕(主屏 + 新副屏)
expect((await browser.screens()).length).toBe(2);
// 4. 清理:按 id 删除副屏
await browser.removeScreen(screenInfo.id);
expect((await browser.screens()).length).toBe(1);
典型场景:把浏览器窗口开到副屏并最大化
addScreen 返回的 availLeft/availTop/availWidth/availHeight 与 BrowserContext.newPage({type: 'window', windowBounds}) 配合,可以精确控制新窗口落在哪块屏幕上。同样的测试 test/src/browser.test.ts 演示了“副屏最大化窗口”流程:
// 添加副屏
const screenInfo = await browser.addScreen({
left: 800,
top: 0,
width: 1600,
height: 1200,
});
// 用返回的 avail* 字段把窗口放到副屏内(四周留 50/100 边距)
const page = await context.newPage({
type: 'window',
windowBounds: {
left: screenInfo.availLeft + 50,
top: screenInfo.availTop + 50,
width: screenInfo.availWidth - 100,
height: screenInfo.availHeight - 100,
},
});
// 通过 page.windowId() 拿到窗口 id,再最大化(windowState: 'maximized')
const windowId = await page.windowId();
await browser.setWindowBounds(windowId, {windowState: 'maximized'});
// 清理
await browser.removeScreen(screenInfo.id);
窗口边界 API(getWindowBounds / setWindowBounds)的文档分别见 Browser.getWindowBounds 与 Browser.setWindowBounds,屏幕枚举见 Browser.screens。
返回对象的默认值速查
从测试断言可以整理出各字段的实际默认行为,便于编写断言或做结果校验:
| 场景 | 主屏(headless 默认 800x600) | 新增副屏(left:800, 1600x1200, insets.bottom:80) |
|---|---|---|
availHeight |
600 | 1120(1200 − 80) |
availLeft / availTop |
0 / 0 | 800 / 0 |
colorDepth |
24(未指定时的默认值) | 32(按入参) |
devicePixelRatio |
1 | 1(未指定时的默认值) |
isPrimary / isExtended |
true / false |
false / true |
orientation |
{angle: 0, type: 'landscapePrimary'} |
{angle: 0, type: 'landscapePrimary'} |
六、小结
Browser.addScreen(params: AddScreenParams): Promise<ScreenInfo>是 Puppeteer 在 headless 模式下构造虚拟多显示器环境的入口:left/top/width/height四个必填项决定屏幕摆放,workAreaInsets、devicePixelRatio、rotation、colorDepth、label、isInternal均为可选修饰项;- 返回值
ScreenInfo中的id是后续removeScreen的唯一句柄,avail*四字段是跨屏放置窗口的可靠坐标来源; - 源码层面它是一次纯粹的 CDP
Emulation.addScreen转发(cdp/Browser.ts),BiDi 连接下不可用(抛UnsupportedOperation); - 典型应用是多屏布局测试、副屏窗口定位与最大化行为验证,完整可运行用例可直接参考 test/src/browser.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