首页
/ Puppeteer HandleOr 类型解析:handle 与原始值统一传参的类型体系

Puppeteer HandleOr 类型解析:handle 与原始值统一传参的类型体系

2026-09-06 18:59:12作者:龚格成

HandleOr<T> 是 Puppeteer(JavaScript API for Chrome and Firefox)公开 API 类型系统中一个精简却关键的联合类型:它把「原始值 T」「JSHandle<T>」与「针对 T 推导出的句柄 HandleFor<T>」收敛为同一种合法入参。本文以 docs/api/puppeteer.handleor.md 为主体,结合其在 packages/puppeteer-core/src/common/types.ts 中的定义及 getPropertyevaluate 等 API 的实现,讲解它的结构、它在句柄参数体系中的枢纽位置,以及它如何支撑"既可传远程对象句柄、又可传普通值"的统一调用约定。

HandleOr 的定义:一个联合类型,三种合法形态

packages/puppeteer-core/src/common/types.ts 中,HandleForHandleOr 紧挨着声明,且 HandleOr 直接复用了 HandleFor

export type HandleFor<T> = T extends Node ? ElementHandle<T> : JSHandle<T>;

export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;

HandleOr<T> 本质是一个参数化的联合类型,拆开来看包含三个分支:

  1. T(原始值):调用方直接传入普通 JavaScript 值。Puppeteer 会在调用执行前将可序列化的值按协议序列化并注入页面上下文,例如字符串、数字、布尔值、普通对象等。
  2. JSHandle<T>:传入一个"远程对象引用"。它代表页面里某个 JavaScript 对象的引用,构造方式见 puppeteer.jshandle.md 对应的 JSHandle 实现——通常由 Page.evaluateHandle 创建,例如 page.evaluateHandle(() => window)。作为参数传入后,Puppeteer 会把句柄解析为它引用的对象本身再交给被求值的函数。
  3. HandleFor<T>T 恰好是 Node 及其子类(如 HTMLDivElement)时,条件类型 T extends Node ? ElementHandle<T> : JSHandle<T> 会把分支收窄为 ElementHandle<T>;否则退回 JSHandle<T>。也就是说 HandleFor<T>HandleOr<T> 而言通常是"重复项",真正扩展出的新形态是原始值 T

从成员关系上可以进一步推断:由于 HandleFor<T> 本身就是 JSHandle<T>(当 TNode 时则是其子类 ElementHandle<T>),HandleOr<T> 实际上等价于 "对应的句柄 或 原始值"。

值得注意:ElementHandle<ElementType extends Node = Element> 继承自 JSHandle<ElementType>(见 ElementHandle.ts),因此在类型层面,元素句柄可以安全地赋值给 HandleOr<T> 中的 HandleFor<T> / JSHandle<T> 分支。

HandleOr 的用途:支撑 evaluate 系列与 getProperty 的灵活入参

单看类型声明很难理解它的价值。真正让它成为枢纽的,是它与 FlattenHandleInnerParams 两个工具类型构成的一套"参数类型双射"机制——同样定义在 common/types.ts

export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;

export type InnerParams<T extends unknown[]> = {
  [K in keyof T]: FlattenHandle<T[K]>;
};

export type EvaluateFunc<T extends unknown[]> = (
  ...params: InnerParams<T>
) => Awaitable<unknown>;

解读这条链路:

  • HandleOr<U> 负责"正向"标注入参类型:API 表面上要求调用方提供 HandleOr<U>,即"句柄或值都行";
  • FlattenHandle 负责"反向"还原对象类型FlattenHandle<HandleOr<U>> 通过 infer U 抽出联合类型中原始值分支对应的 U
  • InnerParams 逐参数应用 FlattenHandle,把"参数数组里每一项可能是句柄"这一约束抹平;
  • 最终传给被求值函数(如 Page.evaluatePage.$evalFrame.evaluate 的回调)的参数被还原为实际的对象类型,因此你在回调函数体内拿到的就是纯 JavaScript 对象,而不是句柄。

于是调用方可以写成 page.evaluate(fn, someHandle)page.evaluate(fn, {a: 1}) 两种等价形式,而函数体内部并不感知差异。这正是 JSHandle 类注释 所声明的行为:"Handles can be used as arguments for any evaluation function such as Page.$eval, Page.evaluate, and Page.evaluateHandle. They are resolved to their referenced object."

典型应用场景一:getProperty 接受句柄型属性名

JSHandle.getPropertyHandleOr 最直接的消费方之一。JSHandle.ts 中的签名与实现如下:

getProperty<K extends keyof T>(
  propertyName: HandleOr<K>,
): Promise<HandleFor<T[K]>>;
getProperty(propertyName: string): Promise<JSHandle<unknown>>;

// @internal
async getProperty<K extends keyof T>(
  propertyName: HandleOr<K>,
): Promise<HandleFor<T[K]>> {
  return await this.evaluateHandle((object, propertyName) => {
    return object[propertyName as K];
  }, propertyName);
}

这里 HandleOr<K> 意味着三种写法都合法:

using handle = await page.evaluateHandle(() => window);

// 1) 直接传字符串键名
const a = await handle.getProperty('location');

// 2) 传一个句柄到该键上(例如属性名本身是 Symbol 或动态 key 时)
using keyHandle = await page.evaluateHandle(() => Symbol.for('myKey'));
const b = await handle.getProperty(keyHandle as never);

// 3) 传原始 Symbol / 字符串值
const c = await handle.getProperty('navigator');

底层实现把 propertyName 原样透传给 evaluateHandle,在页面侧执行 object[propertyName]。由于 getProperty 的重载一要求 K extends keyof T,当 T 已知时(例如 ElementHandle<HTMLSelectElement>),属性名还能得到编译期的键名检查;实现细节可结合 test/src/jshandle.test.ts 的用例验证:对一个 {one:1, two:2, three:3} 对象句柄执行 getProperty('two')jsonValue() 应为 2。当 getProperty 作用在元素句柄上时,ElementHandle.ts 会覆写该方法并转发给内部委托的 this.handle,例如从 <h1> 元素句柄读取 tagName(见 ariaqueryhandler.test.ts)。

典型应用场景二:作为 evaluate / evaluateHandle / $eval 的实参类型

getProperty 外,HandleOr 更多是作为"隐藏的入参约束"贯穿整套求值 API。以文档页 puppeteer.page.evaluatehandle.mdpuppeteer.page._eval.md 涉及的方法为例,它们的泛型参数 Params extends unknown[] 会经过 EvaluateFuncWith/InnerParams 展开,最终每个实参的类型都被约束为某个 HandleOr<U>。体现在调用侧:

// 值传递:handle 会被解析为其引用的对象
using navHandle = await page.evaluateHandle(() => navigator);
const ua = await page.evaluate(e => e.userAgent, navHandle);

// 原始值传递:直接序列化
const text = await page.evaluate(e => e.userAgent, navigator);

// 混合传递:句柄与普通值可以出现在同一次调用里
await page.$eval('input', (el, value) => { el.value = value; }, 'hello');

其中第三种写法中 el 是 DOM 元素、value 是字符串,二者分别走 ElementHandle 分支与原始值分支,而这些分支正是在 HandleOr 的联合中被统一建模的。测试 jshandle.test.ts 也覆盖了"以句柄传对象、以句柄传基本类型"两类调用路径。

返回值与运行机制:为什么这里总是产生新的句柄

HandleOr 只约束入参;出参方向则由 HandleFor 收敛。例如 getProperty 返回 HandleFor<T[K]>evaluateHandle 返回 HandleFor<Awaited<ReturnType<Func>>>,即:只要求值结果来自页面侧对象,就统一包成 JSHandle;当结果的静态类型是 Node 时进一步推断为 ElementHandle

配套的运行机制决定了"传入句柄、传出句柄"都是廉价的引用操作,但必须注意生命周期:

  • JSHandle 会阻止其引用的页面对象被垃圾回收,直到显式调用 JSHandle.dispose(参见 JSHandle 类注释);
  • 当所属 frame 发生导航或父级上下文销毁时,句柄会被自动释放,因此长期持有句柄前应考虑到页面跳转;
  • 句柄相关操作带 @throwIfDisposed() 装饰器(如 JSHandle.tsElementHandle.ts),对已释放句柄调用 getProperty 等会抛出异常;
  • 从源码结构看,跨 realm 的元素句柄通过"隔离句柄"(isolatedHandle,见 ElementHandle.ts)做中转再执行属性读取,以保证在主世界与隔离世界中引用一致。

HandleForJSHandle 的完整 API 文档可分别参考 puppeteer.handlefor.mdpuppeteer.jshandle.md,元素句柄见 puppeteer.elementhandle.md

关联类型一览:一段可整体理解的类型族

为方便在大型代码库中快速定位,这里把 HandleOr 所在的类型族整理如下(全部位于 packages/puppeteer-core/src/common/types.ts):

类型 作用 声明位置
HandleFor<T> 把类型映射为对应句柄:NodeElementHandle<T>,否则 → JSHandle<T> common/types.ts#L66
HandleOr<T> 句柄(HandleFor<T> / JSHandle<T>)或原始值 T 的联合 common/types.ts#L71
FlattenHandle<T> HandleOr 中反解出对象类型 U common/types.ts#L76
InnerParams<T> 把参数元组逐项 FlattenHandle common/types.ts#L81-L83
EvaluateFunc<T> / EvaluateFuncWith<V, T> InnerParams 约束回调的参数类型 common/types.ts#L99-L108

对应 API 文档页还包括 puppeteer.flattenhandle.mdpuppeteer.innerparams.mdpuppeteer.evaluatefunc.mdpuppeteer.evaluatefuncwith.md

小结:HandleOr 是 Puppeteer 传参类型统一的支点

一句话概括:HandleOr<T> 让"远程句柄"与"普通值"在 Puppeteer 的求值与取属性 API 中成为同构入参,配合 FlattenHandle 反向还原出真实对象类型,最终在回调里以纯对象形态出现。它既服务于 getProperty(propertyName: HandleOr<K>) 这类属性访问场景,也隐形约束着 evaluate / evaluateHandle / $eval 等核心方法的实参。理解这一个联合类型,就能同时读懂 Puppeteer 句柄参数体系、返回值句柄推导规则,以及句柄的引用生命周期约束——这是编写健壮的 Puppeteer 自动化脚本与上层类型封装时常被忽略却至关重要的一环。

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