首页
/ Puppeteer 多屏模拟实战:Browser.screens、addScreen 与 removeScreen 接口深度解析

Puppeteer 多屏模拟实战:Browser.screens、addScreen 与 removeScreen 接口深度解析

2026-09-04 22:41:51作者:宣利权Counsellor

Puppeteer 的 Browser 类通过 screens()addScreen()removeScreen() 三个方法,暴露了 Chromium 底层 Emulation 域的多屏模拟能力:查询当前屏幕列表、动态增删虚拟屏幕。在 headless 模式下,开发者可以构造出精确可控的多显示器环境(分辨率、DPI、色深、工作区缩进、屏幕朝向),用于验证窗口最大化、跨屏拖拽、window.screen API 行为等与多屏布局相关的 Web 应用功能。读完本文,你将掌握这三个方法的完整签名、参数与返回值结构、底层 CDP 调用链,以及一套来自官方测试套件的真实多屏构建与清理流程。

核心 API:Browser.screens()

Browser.screens() 用于获取当前浏览器实例下的屏幕信息对象列表,方法签名为:

class Browser {
  abstract screens(): Promise<ScreenInfo[]>;
}
  • 返回值Promise<ScreenInfo[]>,即一组 ScreenInfo 对象;
  • 无参数:屏幕列表反映的是浏览器进程当前"看到"的屏幕布局,headless 模式下即为虚拟屏幕,headful 模式下则是平台真实屏幕。

抽象方法定义位于 Browser.ts 第 830–833 行,其注释与 API 文档一致:

Gets a list of screen information objects.(获取屏幕信息对象列表)

需要特别注意的是测试环境的差异:官方测试 browser.test.ts 明确标注,headful 模式下 screens() 返回的是真实平台屏幕信息,"not stable enough for matching"(不够稳定,无法用于断言匹配),因此该用例在 headful 模式下直接跳过。这说明:screens() 的断言式验证只在 headless 模式具有确定性,headful 下结果取决于运行机器的实际显示环境。

ScreenInfo 返回结构

ScreenInfo 接口在 Browser.ts 中定义,字段与 ScreenInfo API 文档一一对应:

属性 类型 含义
id string 屏幕唯一标识,供 removeScreen() 使用
label string 屏幕标签名,可为空字符串
left / top number 屏幕在全局坐标系中的左上角位置
width / height number 屏幕物理分辨率
availLeft / availTop number 可用区域(扣除任务栏等)左上角位置
availWidth / availHeight number 可用区域尺寸
devicePixelRatio number 设备像素比
colorDepth number 色深(位/像素)
orientation ScreenOrientation 朝向,含 angletype 两个字段
isPrimary boolean 是否主屏
isExtended boolean 是否扩展屏
isInternal boolean 是否为内部(设备自带)屏幕

其中 ScreenOrientation 结构(Browser.ts)为:

export interface ScreenOrientation {
  angle: number;
  type: string;
}

在 headless 默认配置下,官方测试断言了完整的默认主屏形态(browser.test.ts):

const screenInfos = await browser.screens();
expect(screenInfos).toMatchObject([
  {
    availHeight: 600,
    availLeft: 0,
    availTop: 0,
    availWidth: 800,
    colorDepth: 24,
    devicePixelRatio: 1,
    height: 600,
    id: expect.any(String),
    isExtended: false,
    isInternal: false,
    isPrimary: true,
    label: '',
    left: 0,
    orientation: {angle: 0, type: 'landscapePrimary'},
    top: 0,
    width: 800,
  },
]);

这给出了一个可直接复用的基准事实:headless Chrome 的默认主屏为 800x600、色深 24、DPR 1、横向主屏朝向,后续所有 addScreen() 的坐标计算(例如把副屏摆到主屏右侧 left: 800)都以此为参照系。

添加虚拟屏幕:Browser.addScreen()

addScreen(params) 向当前浏览器实例添加一块新屏幕,并返回添加后的 ScreenInfo 对象:

class Browser {
  abstract addScreen(params: AddScreenParams): Promise<ScreenInfo>;
}

Remarks(文档原文):Only supported in headless mode.(仅在 headless 模式下支持)

AddScreenParams 参数表

参数类型 AddScreenParamsBrowser.ts 中的完整定义为:

export interface AddScreenParams {
  left: number;
  top: number;
  width: number;
  height: number;
  workAreaInsets?: WorkAreaInsets;
  devicePixelRatio?: number;
  rotation?: number;
  colorDepth?: number;
  label?: string;
  isInternal?: boolean;
}

结合 AddScreenParams API 文档

参数 类型 必填 说明
left number 新屏幕在全局坐标系中的水平位置
top number 新屏幕在全局坐标系中的垂直位置
width number 屏幕宽度
height number 屏幕高度
workAreaInsets WorkAreaInsets 可用区域相对整屏的边距(top/left/bottom/right,均为可选 number)
devicePixelRatio number 设备像素比,未指定时默认 1
rotation number 屏幕旋转角度
colorDepth number 色深,不传则沿用浏览器默认(headless 下为 24)
label string 屏幕标签
isInternal boolean 是否标记为内部屏幕

其中 WorkAreaInsets 定义(Browser.ts):

export interface WorkAreaInsets {
  top?: number;
  left?: number;
  bottom?: number;
  right?: number;
}

workAreaInsets 的直接影响体现在返回的 avail* 字段上:可用区域 = 整屏尺寸减去各方向 inset。这一语义在官方测试中有精确验证(browser.test.ts):

describe('Browser.add|removeScreen', function () {
  it('should add and remove a screen', async () => {
    const {browser} = await getTestState();
    const screenInfo = await browser.addScreen({
      left: 800,
      top: 0,
      width: 1600,
      height: 1200,
      colorDepth: 32,
      workAreaInsets: {bottom: 80},
      label: 'secondary',
    });
    expect(screenInfo).toMatchObject({
      availHeight: 1120,      // 1200 - 80:底部 inset 生效
      availLeft: 800,
      availTop: 0,
      availWidth: 1600,
      colorDepth: 32,
      devicePixelRatio: 1,    // 未指定,默认 1
      height: 1200,
      id: expect.any(String),
      isExtended: true,       // 自动标记为扩展屏
      isInternal: false,
      isPrimary: false,
      label: 'secondary',
      left: 800,
      orientation: {angle: 0, type: 'landscapePrimary'},
      top: 0,
      width: 1600,
    });
    expect((await browser.screens()).length).toBe(2);

    await browser.removeScreen(screenInfo.id);
    expect((await browser.screens()).length).toBe(1);
  });
});

从这个真实用例可以读出几条确定的运行时行为:

  1. 新增屏幕的 isPrimary 固定为 falseisExtendedtrue——主屏地位始终属于浏览器自带的默认屏幕;
  2. workAreaInsets: {bottom: 80} 使 availHeight 从 1200 缩减为 1120,而 availWidth 不受影响;
  3. labelcolorDepth 等可选字段原样回显在返回的 ScreenInfo 中;
  4. 添加成功后 screens() 列表长度从 1 变为 2,验证了"新增屏幕会立即进入屏幕列表"。

移除虚拟屏幕:Browser.removeScreen()

removeScreen(screenId)id 移除一块已添加的屏幕:

class Browser {
  abstract removeScreen(screenId: string): Promise<void>;
}
  • 参数screenId: string,取自 screens()addScreen() 返回的 ScreenInfo.id
  • 返回值Promise<void>
  • Remarks(文档原文):Only supported in headless mode. Fails if the primary screen id is specified.(仅支持 headless 模式;传入主屏 id 会失败)

第二条限制在构造测试环境时非常关键:默认主屏(isPrimary: true 的那块)不可移除。这保证了多屏测试场景下始终存在一个稳定的主屏参照系,副屏可以按需增删。上面测试用例最后的 await browser.removeScreen(screenInfo.id) 之后,screens() 长度回到 1,正是这一"增—查—删—查"闭环的验证方式。

底层实现:CDP 调用链与协议支持边界

从源码结构看,这三个方法在 CDP 实现中是一组非常薄的透传封装。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});
}

调用链清晰:

Puppeteer 方法 CDP 命令 响应字段
browser.screens() Emulation.getScreenInfos screenInfos
browser.addScreen(params) Emulation.addScreen screenInfo
browser.removeScreen(screenId) Emulation.removeScreen

也就是说,参数没有经过任何本地转换,直接作为 Emulation.addScreen 的参数对象下发,返回的 ScreenInfo 也是浏览器原样回传的结构。调试时如果参数未生效,问题基本可以定位在 Chromium 侧的校验(例如非 headless 模式拒绝 addScreen/removeScreen)。

适用前提:仅 CDP + headless

有两层硬性限制,写进代码前必须确认:

  1. 模式限制addScreenremoveScreen 仅支持 headless 模式(两个方法的 @remarks 均如此声明)。screens() 在 headful 下也能调用,但返回的是真实平台屏幕,结果不可控;
  2. 协议限制:WebDriver BiDi 实现中这三个方法全部抛出不支持异常bidi/Browser.ts
override screens(): Promise<ScreenInfo[]> {
  throw new UnsupportedOperation();
}

override addScreen(_params: AddScreenParams): Promise<ScreenInfo> {
  throw new UnsupportedOperation();
}

override removeScreen(_screenId: string): Promise<void> {
  throw new UnsupportedOperation();
}

因此本文所有用法都要求:Chromium 系浏览器 + CDP 连接 + headless 模式puppeteer.launch() 默认即满足)。

实战:构建双屏环境并放置窗口

把上面三个方法串起来,就得到一个完整的多屏测试套路。官方测试中"最大化窗口"用例(browser.test.ts)展示了典型用法:先在主屏右侧添加副屏,再把窗口开到副屏上,最后调用 setWindowBounds 将其最大化:

// 1. 添加一块 1600x1200 的副屏,紧贴主屏(800 宽)右侧
const screenInfo = await browser.addScreen({
  left: 800,
  top: 0,
  width: 1600,
  height: 1200,
});

// 2. 以副屏的可用区域为基准,在副屏上开窗
const page = await context.newPage({
  type: 'window',
  windowBounds: {
    left: screenInfo.availLeft + 50,
    top: screenInfo.availTop + 50,
    width: screenInfo.availWidth - 100,
    height: screenInfo.availHeight - 100,
  },
});

// 3. 通过 windowId 查询/设置窗口状态,验证跨屏最大化行为
const windowId = await page.windowId();
await browser.setWindowBounds(windowId, {windowState: 'maximized'});

这个流程的价值在于:screenInfo.availLeft / availTop / availWidth / availHeight 让窗口坐标的计算与具体屏幕几何解耦——无论副屏放在哪里、带什么 workAreaInsets,窗口都能精确落在副屏可用区域内。配合 browser.screens() 随时可以断言屏幕布局是否符合预期,测试结束后用 removeScreen(screenInfo.id) 清理,互不污染。

小结

  • Browser.screens()文档):返回 ScreenInfo[],headless 下结果确定(默认 800x600 主屏),适合做布局断言;
  • Browser.addScreen(params)文档):必填 left/top/width/height,可选 workAreaInsets/devicePixelRatio/rotation/colorDepth/label/isInternal;新增屏幕自动标记为 isExtended: true, isPrimary: false;仅 headless 支持;
  • Browser.removeScreen(screenId)文档):按 id 删除屏幕,主屏 id 不可删;仅 headless 支持;
  • 三者均依赖 CDP Emulation 域命令(getScreenInfos / addScreen / removeScreen),在 WebDriver BiDi 模式下抛出 UnsupportedOperation,使用前请确认运行在 Chromium + CDP + headless 的组合下。
登录后查看全文
热门项目推荐
相关项目推荐