Puppeteer 无头屏幕配置实战:--screen-info 静态多屏与 addScreen 动态屏幕管理
本指南以 Puppeteer 官方文档 screen-configuration.md 为骨架,系统讲解如何让无头(headless)Chrome 使用真实的多显示器屏幕拓扑:既可通过启动参数 --screen-info 在浏览器启动前一次性描述多个屏幕及其位置、尺寸、方向与标签,也可以在浏览器运行期间通过 Browser.addScreen / Browser.removeScreen 动态增删屏幕,并用 Browser.screens 随时读取当前屏幕集合。读完本文你将掌握多屏无头环境下 Web 应用“跨屏布局、副屏窗口最大化、多屏渲染”等场景的测试与模拟方法。
为什么需要为无头 Chrome 配置屏幕
物理机上的浏览器窗口管理、screen.availWidth/availHeight 等 Web API 都依赖操作系统提供的屏幕信息。而在 headless 模式下,Chrome 没有真实物理屏幕,默认只会模拟一块逻辑屏幕(详见下文“无开关时的默认屏幕”)。如果你的被测页面需要验证多屏布局、副屏弹出窗口位置、双屏拼接广告、竖屏适配等行为,就必须先为无头浏览器构造出符合预期的“虚拟屏幕拓扑”。
该能力在 Chromium 侧由 headless 组件中的 screen_info 实现支撑(仓库文档原文注明了对应 Chromium 内部 README),而在 Puppeteer 侧则暴露为两个层次的 API:静态的 --screen-info 启动开关与动态的 Browser.addScreen/removeScreen 方法。
使用 --screen-info 开关配置双屏启动环境
--screen-info 是一个传给 Chrome 的命令行开关,用于在启动阶段配置 headless 屏幕。其字符串语法形如:{800x600 label=1st}{600x800 label=2nd}——每个屏幕用一对花括号描述,内部为 宽度x高度,可附带 label=xxx 命名。
下面的脚本让 Chrome 运行在“双屏”环境:主屏 800x600 横屏(landscape),副屏 600x800 竖屏(portrait),且副屏紧贴主屏右侧(即副屏 left 偏移量等于主屏宽度 800):
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({
args: ['--screen-info={800x600 label=1st}{600x800 label=2nd}'],
});
const screens = await browser.screens();
const screenInfos = screens.map(
s =>
`Screen [${s.id}]` +
` ${s.left},${s.top} ${s.width}x${s.height}` +
` label='${s.label}'` +
` isPrimary=${s.isPrimary}` +
` isExtended=${s.isExtended}` +
` isInternal=${s.isInternal}` +
` colorDepth=${s.colorDepth}` +
` devicePixelRatio=${s.devicePixelRatio}` +
` avail=${s.availLeft},${s.availTop} ${s.availWidth}x${s.availHeight}` +
` orientation.type=${s.orientation.type}` +
` orientation.angle=${s.orientation.angle}`,
);
console.log(`Number of screens: ${screens.length}\n` + screenInfos.join('\n'));
await browser.close();
运行输出:
Number of screens: 2
Screen [1] 0,0 800x600 label='1st' isPrimary=true isExtended=true isInternal=false colorDepth=24 devicePixelRatio=1 avail=0,0 800x600 orientation.type=landscapePrimary orientation.angle=0
Screen [2] 800,0 600x800 label='2nd' isPrimary=false isExtended=true isInternal=false colorDepth=24 devicePixelRatio=1 avail=800,0 600x800 orientation.type=portraitPrimary orientation.angle=0
注意输出中的两个关键推论:
- 第一个声明的屏幕自动成为主屏(
isPrimary=true),位于虚拟桌面左上角(0,0);第二个屏幕的left=800说明它从主屏右边缘开始,由此构成“主屏在左、副屏在右”的扩展桌面。 - 方向(orientation)由宽高自动推导:
800x600(宽大于高)为landscapePrimary,600x800(高大于宽)为portraitPrimary,旋转角angle均为 0。这是与 ChromiumEmulation.getScreenInfos返回结果一致的行为(见下文源码部分)。
各 ScreenInfo 字段语义
browser.screens() 返回的是 ScreenInfo 对象的数组。其完整结构定义于 api/Browser.ts,字段含义如下:
| 字段 | 类型 | 含义 |
|---|---|---|
id |
string | 屏幕唯一标识,示例输出中显示为 [1]、[2],也是 removeScreen 需要的参数 |
left / top |
number | 屏幕在虚拟桌面中的左上角坐标 |
width / height |
number | 屏幕逻辑分辨率 |
availLeft / availTop / availWidth / availHeight |
number | 去掉任务栏/工作区留白后页面可用的工作区矩形 |
devicePixelRatio |
number | 设备像素比(示例中为 1) |
colorDepth |
number | 色深(示例中为 24) |
orientation |
ScreenOrientation | 方向对象,含 type(如 landscapePrimary/portraitPrimary)与 angle(旋转角) |
isExtended |
boolean | 是否为扩展屏(单屏时为 false,多屏时为 true) |
isInternal |
boolean | 是否内建屏幕(headless 模拟屏为 false) |
isPrimary |
boolean | 是否主屏 |
label |
string | 屏幕名称,对应 --screen-info 中的 label= 或 addScreen 传入的 label |
接口本身的 availWidth/availHeight/availLeft/availTop、colorDepth 等均声明在 puppeteer.screeninfo.md 的 API 参考页中。
无 --screen-info 开关时的默认屏幕
如果不传 --screen-info,headless 默认只有一块 800x600 的屏幕;但若同时指定了 --window-size 开关,则 headless 屏幕会放大到请求的窗口尺寸。也就是说,--screen-info 是比 --window-size 更精细、能力更强的屏幕控制手段——前者描述的是“屏幕硬件拓扑”,后者只影响单个窗口/视口大小。
仓库的集成测试也印证了该用法:在 page.test.ts 中,Page.resize 相关测试专门通过 args: ['--screen-info={3840x2160}'] 启动一个 4K 无头屏,并配合 page.setViewport(null) 移除默认 800x600 视口对窗口尺寸的限制,进而验证浏览器窗口可按页面内容尺寸自动调整。这说明要测试大屏或窗口自适应逻辑时,先声明一块足够大的屏幕是必要前提。
:::caution 使用前提
--screen-info 开关只在 headless 模式下生效。Headful(有头)Chrome 始终使用操作系统真实物理屏幕,该开关会被忽略。
:::
动态屏幕配置:运行期 addScreen / removeScreen / screens
除了启动时一次性声明外,Puppeteer 还允许在 Chrome 运行期间动态调整屏幕集合:
Browser.screens():读取当前屏幕配置;Browser.addScreen(params):新增一个屏幕并返回其ScreenInfo;Browser.removeScreen(screenId):移除一个屏幕。
下面的脚本先以单屏启动,随后动态添加一个位于右侧的 800x600 副屏,再将其移除,全程打印每一步的屏幕配置:
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({
args: ['--screen-info={800x600 label=1st}'],
});
function getScreenInfo(s) {
return (
`Screen [${s.id}]` +
` ${s.left},${s.top} ${s.width}x${s.height}` +
` label='${s.label}'` +
` isPrimary=${s.isPrimary}` +
` isExtended=${s.isExtended}`
);
}
async function logScreenConfig(text) {
if (text !== undefined) {
console.log(text);
}
const screens = await browser.screens();
const screenInfos = screens.map(s => getScreenInfo(s));
console.log(
`Number of screens: ${screens.length}\n` + screenInfos.join('\n'),
);
}
await logScreenConfig('---- Initial:');
// Add a screen.
const addedScreenInfo = await browser.addScreen({
left: 800,
top: 0,
width: 800,
height: 600,
label: '2nd',
});
console.log('Added screen: ' + getScreenInfo(addedScreenInfo));
await logScreenConfig('---- With the screen added:');
// Remove the added screen.
await browser.removeScreen(addedScreenInfo.id);
await logScreenConfig('---- With added screen removed:');
await browser.close();
对应输出:
---- Initial:
Number of screens: 1
Screen [1] 0,0 800x600 label='1st' isPrimary=true isExtended=false
Added screen: Screen [2] 800,0 800x600 label='2nd' isPrimary=false isExtended=true
---- With the screen added:
Number of screens: 2
Screen [1] 0,0 800x600 label='1st' isPrimary=true isExtended=true
Screen [2] 800,0 800x600 label='2nd' isPrimary=false isExtended=true
---- With added screen removed:
Number of screens: 1
Screen [1] 0,0 800x600 label='1st' isPrimary=true isExtended=false
输出清晰展示了 isExtended 的语义变化:单屏时主屏 isExtended=false,一旦存在第二块屏幕,两块屏的 isExtended 都变为 true;移除副屏后回到初始状态。
AddScreenParams:动态新增屏幕可控制哪些属性
addScreen 的参数类型 AddScreenParams 定义在 api/Browser.ts,除必需的几何位置外,还支持若干可选属性:
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
left / top |
number | 必填 | 新屏幕在虚拟桌面中的左上角坐标 |
width / height |
number | 必填 | 新屏幕的分辨率 |
workAreaInsets |
WorkAreaInsets | 可选 | 工作区四周留白 {top, left, bottom, right},用于模拟任务栏等占据的区域 |
devicePixelRatio |
number | 可选 | 设备像素比 |
rotation |
number | 可选 | 屏幕旋转角度,会反映到 orientation.angle |
colorDepth |
number | 可选 | 色深 |
label |
string | 可选 | 屏幕标签 |
isInternal |
boolean | 可选 | 是否标记为内建屏 |
其中 workAreaInsets 对页面可感知尺寸影响很大:可用工作区 availWidth/availHeight = 屏幕 width/height 减去对应方向的 insets。仓库测试 browser.test.ts 便验证了这一关系——向 addScreen 传入 width: 1600, height: 1200, workAreaInsets: {bottom: 80} 后,断言返回结果满足 availHeight: 1120(即 1200 - 80)、availWidth: 1600、colorDepth: 32、devicePixelRatio: 1、orientation: {angle: 0, type: 'landscapePrimary'} 等,同时 isPrimary: false、isExtended: true、id 为任意字符串。这说明 addScreen 返回的对象就是 Chrome 内部屏幕状态的真实回显。
动态副屏的实际用途
动态副屏最常见的价值是配合窗口管理 API 使用:可以在副屏上打开窗口并执行最大化。同样在 browser.test.ts 的测试中,先 addScreen 添加一个 1600x1200 副屏,再通过 context.newPage({type: 'window', windowBounds: ...}) 在副屏 avail 区域内开窗,随后用 browser.setWindowBounds(windowId, {windowState: 'maximized'}) 将该窗口最大化到副屏。这组测试证明:多屏无头环境完全支持“在不同屏幕上创建并最大化窗口”的桌面类应用行为验证。
:::caution 使用前提
Browser.addScreen 与 Browser.removeScreen 仅在 headless 模式下可用;Browser.screens 则在 headful 与 headless 两种模式下均可调用。此外,从 api/Browser.ts 中 removeScreen 的源码注释可知:移除主屏(primary screen)会失败——至少保留一块主屏是屏幕拓扑的不变约束。
:::
源码实现:从 Puppeteer API 到 Chromium 命令
从仓库源码结构看,这三组屏幕 API 是一套典型的“协议无关抽象 + 协议实现”体系:
-
协议无关的抽象层:在 api/Browser.ts 中,
Browser基类将screens()、addScreen(params)、removeScreen(screenId)声明为抽象方法,并同步导出ScreenInfo、ScreenOrientation、AddScreenParams、WorkAreaInsets等公开类型。 -
CDP(Chrome DevTools Protocol)实现:在 cdp/Browser.ts 中,三者被映射为三条
Emulation域命令:screens()→Emulation.getScreenInfosaddScreen(params)→Emulation.addScreen(params 原样透传)removeScreen(screenId)→Emulation.removeScreen
-
WebDriver BiDi 协议下的限制:在 bidi/Browser.ts 中,三个方法目前直接抛出
UnsupportedOperation。可以推断,通过 WebDriver BiDi 协议连接的 Firefox/浏览器暂不支持这套屏幕模拟 API,当前仅 CDP 通道(Chromium headless)具备完整能力。
也就是说:--screen-info 与 addScreen/removeScreen 最终都收敛到 Chromium 的 headless 屏幕模拟能力,Puppeteer 只是把 CDP 命令包装成了类型安全的 TypeScript 方法,并在运行时经由 browser.screens() 与页面内 window.screen / screen.avail* 等 Web API 保持一致。
小结与使用建议
- 启动前已知拓扑用
--screen-info:语法为连续花括号块,每块{宽x高 label=名称},先声明者为主屏,方向由宽高比自动推导,屏幕按left/top拼接成虚拟桌面。 - 运行期变化用
Browser.addScreen/Browser.removeScreen:适合在同一个浏览器会话中先后模拟“单屏→双屏→单屏”等切换场景,其中workAreaInsets可用于精细模拟任务栏对avail*的影响。 - 读取现状统一用
Browser.screens():其返回字段与Emulation.getScreenInfos的产出一一对应,可同时用于 headful 与 headless。 - 协议注意:上述屏幕 API 的完整实现位于 CDP 通道;经 WebDriver BiDi 连接时 addScreen/removeScreen/screens 均不可用。且一切屏幕模拟都发生在 headless 模式内,headful 模式始终走真实物理屏幕。
- 与视口/窗口的关系:默认无头屏为 800x600,
--window-size可放大之;若配合--screen-info声明大屏后仍受 800x600 默认视口限制,可参照 page.test.ts 的做法先page.setViewport(null)解除视口约束,再测试窗口级行为。
如需进一步查阅类型细节,可参考 puppeteer.addscreenparams.md、puppeteer.screeninfo.md、puppeteer.browser.addscreen.md 与 puppeteer.browser.removescreen.md 等 API 文档。
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 StartedRust0624
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