首页
/ Puppeteer Touchscreen 类完整指南:多点触控事件模拟的 API 设计与底层原理

Puppeteer Touchscreen 类完整指南:多点触控事件模拟的 API 设计与底层原理

2026-09-07 19:38:47作者:幸俭卉

Touchscreen 是 Puppeteer 中用于派发触摸事件(如 tap、touchstart、touchmove、touchend)的抽象类,是编写移动端页面自动化、手势交互测试与触摸优化调试的入口。读完本文,你将掌握 Touchscreen 四个公开方法的精确语义与参数约定,理解 TouchHandle 多触点句柄机制,并透过 CDP(Chrome DevTools Protocol)与 WebDriver BiDi 两套协议实现看清触摸事件的底层派发链路。

Touchscreen 概述与类签名

Touchscreen 是输入层(Input 模块)的核心抽象之一,与 MouseKeyboardTouchHandle 并列。它直接暴露触摸屏事件:touchstart(手指按下)、touchmove(手指移动)、touchend(手指抬起),并提供了一个组合动作 tap(点击)。

其类型签名定义于 packages/puppeteer-core/src/api/Input.ts#L495-L507

export declare abstract class Touchscreen

几个关键约定:

从源码结构可以推断,Touchscreen 基类内部维护了用于管理多触点状态的增量 ID 生成器 idGenerator 与活动触点数组 touches(见 packages/puppeteer-core/src/api/Input.ts#L499-L503),这为后续的多点触控(multi-touch)能力提供了基础。

获取 Touchscreen 实例

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// 获取当前页面的触摸屏控制对象
const touchscreen = page.touchscreen;

注意:触摸事件属于移动端手势语义,需要运行环境对触摸输入的支持。若希望模拟移动设备体验,可结合设备描述符(如 KnownDevices)或 page.emulate() 设置带触摸的 viewport。

方法一览

Touchscreen 共暴露 4 个公开方法,其中 taptouchMovetouchEnd 在抽象基类中已给出基于 touchStart 的通用实现,唯一必须由子类实现的是 touchStart

方法 签名 派发事件 返回
tap tap(x, y): Promise<void> touchstart + touchend
touchStart touchStart(x, y): Promise<TouchHandle> touchstart 新触点的句柄
touchMove touchMove(x, y): Promise<void> 在第一个活动触点上触发 touchmove
touchEnd touchEnd(): Promise<void> 在第一个活动触点上触发 touchend

其中坐标参数含义:

  • x:触摸点的水平位置(Horizontal position);
  • y:触摸点的垂直位置(Vertical position)。

坐标经过协议层传递后作用于当前页面的触摸输入,因此应使用与页面内容对应的视口内坐标(CSS 像素),例如先用 elementHandle.boundingBox() 取得元素位置再发起触摸。

tap():最常用的点击手势

tap() 派发一对 touchstarttouchend 事件,模拟一次“按下后立即抬起”的轻点操作:

class Touchscreen {
  tap(x: number, y: number): Promise<void>;
}
  • 参数 x:点击位置的水平坐标;
  • 参数 y:点击位置的垂直坐标;
  • 返回 Promise<void>

从源码看,tap 是原子化组合动作——先调用 touchStart(x, y) 建立触点,随即对该触点调用 end()(见 packages/puppeteer-core/src/api/Input.ts#L525-L528):

async tap(x: number, y: number): Promise<void> {
  const touch = await this.touchStart(x, y);
  await touch.end();
}

因此在实际派发事件的角度,tap 等价于一次完整的“按下→释放”,适合点击按钮、切换开关等常见移动端交互断言。若你的目标元素坐标已知,写法如下:

// 点击页面 (100, 200) 处的坐标
await page.touchscreen.tap(100, 200);

除 Touchscreen 本身的 tap 外,Puppeteer 的 Page.tap()ElementHandle.tap() 也会走相同的触摸派发路径,差别在于它们会先定位并滚动到元素、再计算可点击坐标。

细粒度手势:touchStart / touchMove / touchEnd

对于滑动(swipe)、长按(long press)、拖拽式手势这类需要多步时序的交互,单靠 tap 不够,需要使用 touchStart + touchMove + touchEnd 的组合。这一组方法的特征是面向“当前活动触点集合”操作:在默认(单指)场景下,它们作用的对象是“第一个活动触点”。

touchStart(x, y):建立触点并返回句柄

touchStart() 派发 touchstart 事件,将新的触点压入活动触点队列,并返回该触点的 TouchHandle 用于后续单独控制:

class Touchscreen {
  abstract touchStart(x: number, y: number): Promise<TouchHandle>;
}
  • 参数 x / y:触点起始位置坐标;
  • 返回 Promise<TouchHandle>:新建立触点的句柄。

它是 Touchscreen 唯一的抽象方法(packages/puppeteer-core/src/api/Input.ts#L536),即协议层差异完全收敛于此:CDP 实现负责分配触点在协议层的增量 id 并发送 Input.dispatchTouchEvent(type 为 touchStart),BiDi 实现则通过指针动作序列派发触摸 pointerDown

touchMove(x, y):移动第一个活动触点

touchMove() 派发 touchmove 事件,作用对象为当前第一个活动触点

class Touchscreen {
  touchMove(x: number, y: number): Promise<void>;
}
  • 参数 x / y:触点移动到的目标位置;
  • 返回 Promise<void>

基类实现(packages/puppeteer-core/src/api/Input.ts#L550-L556)取 this.touches[0] 并委托给该触点句柄的 move(x, y)

async touchMove(x: number, y: number): Promise<void> {
  const touch = this.touches[0];
  if (!touch) {
    throw new TouchError('Must start a new Touch first');
  }
  return await touch.move(x, y);
}

重要限制(原文档 Remarks,务必留意):并非每一次 touchMove 调用都会产生一次 touchmove 事件,最终是否派发取决于浏览器的优化策略。典型例子是 Chrome 会对 touchmove 事件进行节流(throttling)——这是 Chrome 自引入“受限异步 touchmove 模型”(throttled async touchmove model)以来的既定行为,目的是在不影响页面滚动性能的前提下降低事件频率。因此,如果你的自动化脚本依赖 touchmove 事件的次数做精确断言,结果可能与调用的移动次数不完全一致;应把 touchMove 视为“把触点移动到指定坐标”的位置控制原语,而非逐帧事件发生器。

touchEnd():结束第一个活动触点

touchEnd() 派发 touchend 事件,结束当前第一个活动触点

class Touchscreen {
  touchEnd(): Promise<void>;
}

无参数,返回 Promise<void>。基类实现使用 touches.shift() 从队列头部取出触点(packages/puppeteer-core/src/api/Input.ts#L561-L567),这保证了先按 touchStart 建立的触点会被先 touchend——符合多点手势“先按下先抬起”的常见时序。

组合示例:一次滑动(swipe)

const {x, y} = await element.boundingBox(); // 元素左上角坐标
const ts = page.touchscreen;

await ts.touchStart(x + 10, y + 10); // 手指按下
await ts.touchMove(x + 40, y + 30);  // 分步移动(会被浏览器节流优化)
await ts.touchMove(x + 80, y + 60);
await ts.touchEnd();                 // 手指抬起

把上述坐标插值成多段 touchMove,即可实现轮播图翻页、列表惯性滑动、地图拖动等手势路径的自动化模拟。

多触点(Multi-Touch)与 TouchHandle

Touchscreen 并不仅限于单指。从基类设计(活动触点数组 + 增量 ID 生成器)与 touchStart 返回 TouchHandle 这一点可以推断:同时调用多次 touchStart 即可在同一页面建立多个独立触点,每个触点由各自的 TouchHandle 单独移动与结束,从而实现双指缩放(pinch)、双指旋转等复杂手势。

TouchHandle(接口定义见 packages/puppeteer-core/src/api/Input.ts#L479-L490)对外只暴露两个方法:

export interface TouchHandle {
  move(x: number, y: number): Promise<void>;  // 单独移动该触点
  end(): Promise<void>;                       // 单独结束该触点
}

双指缩放的示意流程:

const ts = page.touchscreen;

const fingerA = await ts.touchStart(100, 200);  // 第一指落下
const fingerB = await ts.touchStart(300, 200);  // 第二指落下

await fingerA.move(80, 200);   // 两指向外/内扩张
await fingerB.move(320, 200);

await fingerA.end();           // 依次抬起
await fingerB.end();

注意:touchMove / touchEnd(作用于“第一个活动触点”)与 TouchHandle.move / TouchHandle.end(作用于指定触点)是两套控制路径。单指手势用前者即可;一旦同时存在多个触点,请优先使用句柄方法精确定位目标触点。

错误处理:未建立触点时抛 TouchError

touchMovetouchEnd 都要求在触点队列中存在活动触点,否则抛出 TouchError,错误消息为 'Must start a new Touch first'(见 packages/puppeteer-core/src/api/Input.ts#L552-L554)。TouchError 定义于 packages/puppeteer-core/src/common/Errors.ts#L46,继承自 PuppeteerError。因此在组合手势前,务必先 await ts.touchStart(x, y) 建立触点,否则会立即收到异常。CDP 实现中若对同一触点重复调用 start(),也会抛出 'Touch has already started'TouchError(见 packages/puppeteer-core/src/cdp/Input.ts#L580-L583)。

底层实现:两条协议路径

CDP(Chrome/Chromium)路径

CdpTouchscreenpackages/puppeteer-core/src/cdp/Input.ts#L615-L652)把每个触点建模为一个 Protocol.Input.TouchPointtouchStart 时生成:

const touchPoint: Protocol.Input.TouchPoint = {
  x: Math.round(x),
  y: Math.round(y),
  radiusX: 0.5,
  radiusY: 0.5,
  force: 0.5,
  id,
};

这里能看到若干实现细节:

  • 坐标取整xyMath.round 转为整数后下发给浏览器;
  • 默认触点几何:触摸半径 radiusX/radiusY 默认 0.5,压力 force 默认 0.5;
  • 唯一 ID:触点 id 来自基类自带的 idGenerator,用于在协议层区分多个并发触点。

真正的事件派发由 Input.dispatchTouchEvent 完成,CdpTouchHandle 依据触点状态分别发送 type: 'touchStart' | 'touchMove' | 'touchEnd'(见 packages/puppeteer-core/src/cdp/Input.ts#L584-L608),同时把键盘修饰键状态(modifiers)一并传入,说明触摸事件与 Keyboard 的修饰键状态是联动的。触点结束时调用基类 removeHandle 从活动队列中移除自身,保持队列与协议状态一致。

WebDriver BiDi(Firefox 等)路径

BidiTouchscreenpackages/puppeteer-core/src/bidi/Input.ts#L716-L741)走 WebDriver BiDi 的指针输入源:touchStart 构建 width/height = 0.5 * 2pressure = 0.5 的触点属性,BidiTouchHandle 通过 pointerDown/pointerMove/pointerUp 动作序列模拟触摸语义(见 packages/puppeteer-core/src/bidi/Input.ts#L618-L712)。

两条路径共享同一抽象 API,这也解释了 Touchscreen 采用抽象类设计的理由——把“触摸语义描述”与“协议字节流”解耦,上层测试代码完全无协议差异。

实战场景小结

结合上文,Touchscreen 的典型应用场景与对应能力如下:

  1. 移动端 UI 自动化:用 tap(x, y) 替代鼠标点击触发移动端点击态样式、:active 等仅在触摸输入下出现的行为;
  2. 手势/可交互测试:用 touchStart + 多段 touchMove + touchEnd 模拟滑动、翻页、长按,验证手势逻辑(注意 touchmove 会被 Chrome 节流,不宜依赖事件次数);
  3. 多点手势测试:并发建立多个触点并借助 TouchHandle.move/end 做双指缩放、旋转;
  4. 统一跨浏览器脚本:通过抽象类 API 编写一套逻辑,分别跑在 CDP 与 WebDriver BiDi 之上,覆盖 Chrome 与 Firefox。

更多触摸相关能力可延伸阅读 TouchHandlePage.tap()TouchScreen.tap() 以及 ElementHandle.touchstart() 等文档。若需阅读底层类型定义,可回到 packages/puppeteer-core/src/api/Input.ts(Touchscreen 抽象基类与 TouchHandle 接口)、packages/puppeteer-core/src/cdp/Input.ts(CDP 实现)与 packages/puppeteer-core/src/bidi/Input.ts(WebDriver BiDi 实现)继续深入。

登录后查看全文
热门项目推荐
相关项目推荐