☰
Puppeteer EventsWithWildcard 类型解析:为事件系统提供通配监听的类型基石
2026/10/20 1:13:00 网站建设 项目流程

Puppeteer EventsWithWildcard 类型解析:为事件系统提供通配监听的类型基石

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

Puppeteer 的事件体系以 EventEmitter 为核心:Page、Browser、BrowserContext、CDPSession、WebWorker等几乎所有关键类都继承自它并向外派发事件。本篇文章聚焦其中的一个基础类型 ——EventsWithWildcard,说明它如何在不破坏每个事件强类型的前提下,为"监听所有事件"这种需求提供编译期支持,并结合仓库源码说明EventEmitter如何将通配键纳入真实的监听簿记,以及Page等类的泛型方法如何依赖它。读完本文,你将理解 Puppeteer 类型化事件映射(Event Map)的设计骨架,并能正确编写通配事件处理代码。

关联文档:docs/api/puppeteer.eventswithwildcard.md

一、类型定义:EventsWithWildcard 究竟是什么

Puppeteer 官方 API 文档给出了这个类型最精炼的定义:

export type EventsWithWildcard<Events extends Record<EventType, unknown>> = Events & { '*': Events[keyof Events]; };

其中EventType定义为string | symbol,意味着一个事件键既可以是普通字符串(例如'request'、'close'),也可以是 Symbol(相关定义见 docs/api/puppeteer.eventtype.md 与 EventEmitter 源码)。

这个类型做的事情可以拆成两步来理解:

  1. 约束输入:泛型参数Events必须是一个以EventType为键的记录类型,也就是 Puppeteer 各事件映射(如PageEvents、BidiEvents)的形态 —— 键是事件名,值是事件负载类型。
  2. 追加通配键:把Events原样保留,同时增加一个键'*',它的负载类型是Events[keyof Events]—— 也就是所有事件负载类型组成的联合类型。

因此,如果有一个自定义事件映射:

interface MyEvents { load: {url: string}; error: {code: number}; }

那么EventsWithWildcard<MyEvents>展开后等效于:

{ load: {url: string}; error: {code: number}; '*': {url: string} | {code: number}; }

从语义上讲,监听'*'意味着"无论哪个事件发生,都能收到回调",而收到的负载不可能是某一个具体类型,只可能是所有负载的联合 —— 因此联合类型是唯一正确的类型表达。这也是该类型名中WithWildcard(带通配)的由来。

二、它为何存在:类型化事件映射的"补全"

在 EventEmitter 源码 中可以看到两个关键接口的配合关系:

// packages/puppeteer-core/src/common/EventEmitter.ts export interface CommonEventEmitter<Events extends Record<EventType, unknown>> { on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): this; off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): this; emit<Key extends keyof Events>(type: Key, event: Events[Key]): boolean; once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): this; listenerCount(event: keyof Events): number; removeAllListeners(event?: keyof Events): this; }

而EventEmitter类在实现该接口时,把所有方法签名中的Events都替换成了EventsWithWildcard<Events>(见 EventEmitter.ts 源码):

export class EventEmitter< Events extends Record<EventType, unknown>, > implements CommonEventEmitter<EventsWithWildcard<Events>> {

implements泛型实例化使用的是EventsWithWildcard<Events>,于是类内所有方法的Key空间都被"补全"成了keyof Events | '*'。例如官方文档中的 EventEmitter.on() 签名即是:

on<Key extends keyof EventsWithWildcard<Events>>( type: Key, handler: Handler<EventsWithWildcard<Events>[Key]>, ): this;

同理,off、once、emit、listenerCount、removeAllListeners 全都以EventsWithWildcard<Events>作为键空间来源。换句话说,EventsWithWildcard是让 EventEmitter 全部 API 允许'*'键的单一开关—— 改动这一个类型,所有方法的类型能力就同步更新,这是这套设计的精妙之处。

三、类型收益:通配事件负载是联合类型

把通配键接入之后,实际开发中就能写出这样的监听代码:

page.on('*', event => { // event 的编译期类型是 Page 全部事件负载的联合类型 // 运行时需要借助类型守卫进行收窄 if (typeof event === 'string') { // 处理字符串类型的负载(例如 frame 名称等) } else { // 处理对象类型的负载 } });

注意这里'*'并不会干扰对其他具体事件的强类型:由于EventsWithWildcard<Events> = Events & { '*': ... },on('load', handler)仍然精确地推导出handler的参数就是load事件的负载类型,类型安全没有被破坏。通配键与具体键共存,互不干扰。这正是"交集类型 + 追加键"这种写法的核心价值:在不覆盖原事件映射任何键的前提下追加一个特殊键。

由此也引出一个实用的编码建议:监听'*'时,编译器给到的是联合类型,不要试图直接访问某一个具体负载才有的属性,应当先做收窄(typeof 判断、in操作符或自定义类型守卫),再处理业务逻辑。而once('*', handler)语义同样成立 —— 只响应"下一个任意事件"。

四、源码纵深:EventEmitter 如何把 '*' 当作真实监听键

如果只在类型层面做文章,通配键并不能真正工作。让我们看 EventEmitter 类实现 的内部簿记结构:

#handlers = new Map<keyof Events | '*', Array<Handler<any>>>();

这是一个以"事件键"为键的处理器数组映射,而键的联合类型明确包含'*'。也就是说,在实现层面'*'与任何具体事件名地位完全平等 —— 向on('*', handler)注册时:

on<Key extends keyof EventsWithWildcard<Events>>(type: Key, handler: Handler<...>): this { const handlers = this.#handlers.get(type); if (handlers === undefined) { this.#handlers.set(type, [handler]); } else { handlers.push(handler); } this.#emitter.on(type, handler); return this; }

on直接把type(可以是'*')当作映射键,先把 handler 存进自己的#handlers,再转发给内部包装的 mitt emitter(EventEmitter 默认以mitt(new Map())作为底层发射器,见 构造函数)。这里的#handlers是同步簿记,而底层 emitter 负责真实派发。

以'*'为键参与簿记的意义体现在各方法的一致性上:

  • listenerCount(type)(源码):return this.#handlers.get(type)?.length || 0;—— 传'*'时返回通配监听器数量,因而emit的返回值("是否存在监听者")对通配键同样有效;
  • off(type, handler)(源码):不传 handler 时遍历并解绑该键下所有监听并删除该键;传 handler 时按lastIndexOf找到并移除 —— 解绑'*'与解绑普通事件走同一条路径;
  • once(type, handler)(源码):用一个包装函数在首次触发后自动调用this.off(type, onceHandler)完成一次性解绑,对'*'同样适用;
  • removeAllListeners(type)(源码):传入 type 时等价于off(type),即只清除该事件(含'*')的监听。

此外,EventEmitter还实现了 JS 的显式资源管理协议,即文档中列出的[asyncDisposeSymbol]()与[disposeSymbol]()两个方法(见 docs/api/puppeteer.eventemitter.md 方法表)。dispose 时会遍历#handlers全部键(包括'*')逐一解绑底层监听并清空映射(实现源码)。这保证即使以'*'注册了大量监听,销毁 EventEmitter 也不会留下泄漏。

值得强调的是:EventsWithWildcard回答的是"如何让'*'成为类型系统与簿记系统里合法的键",而具体某个事件是否在运行时被广播给'*'监听者,取决于事件发射端的 emit 实现与广播策略,这一点在使用自定义 EventEmitter 时需结合实际派发路径确认。本文以文档与源码能确证的类型与簿记设计为主线,不展开臆断运行时的具体广播细节。

五、真实应用场景:Page 与 WebDriver BiDi 连接的泛型签名

EventsWithWildcard并非孤立存在的辅助类型,它在仓库多个核心类的方法签名中直接出现,可以从两类真实用法中看到它的位置。

5.1 Page:override on/off 时的键空间

Page.ts 在第 50 行导入了EventsWithWildcard,随后重写了on与off:

// packages/puppeteer-core/src/api/Page.ts override on<K extends keyof EventsWithWildcard<PageEvents>>( type: K, handler: (event: EventsWithWildcard<PageEvents>[K]) => void, ): this { ... } override off<K extends keyof EventsWithWildcard<PageEvents>>( type: K, handler: (event: EventsWithWildcard<PageEvents>[K]) => void, ): this { ... }

Page的PageEvents事件映射覆盖了请求/响应、导航、对话框、Worker、截图等全部页面事件(事件常量见 docs/api/puppeteer.pageevent.md,完整事件枚举见 docs/api/puppeteer.pageevents.md)。通过把PageEvents交给EventsWithWildcard,page.on/page.off既保留了针对PageEvent.Request这类具体键的精确推导,又把'*'纳入合法的 type 参数。Page之所以要 override 这两个方法,是因为它对Request事件做了特殊接线:为了协作式请求拦截,监听会被包装进enqueueInterceptAction回调(见 Page.ts on 实现),而off时需要还原出原始 handler 才能正确解绑(Page.ts off 实现)—— 泛型签名中保留EventsWithWildcard<PageEvents>,正是为了让这种包装逻辑对普通键与通配键都保持一致的类型体验。

5.2 WebDriver BiDi 连接:override emit 时的键空间

在 WebDriver BiDi 协议连接中,bidi/Connection.ts 同样从../common/EventEmitter.js导入了该类型(第 14 行),并以此重写emit:

override emit<Key extends keyof EventsWithWildcard<BidiEvents>>( key: Key, event: EventsWithWildcard<BidiEvents>[Key], ): boolean { ... }

这说明EventsWithWildcard是puppeteer-core的common/EventEmitter模块对外导出的公共类型(@public),被 Chromium/CDP 侧(Page)与 Firefox/WebDriver BiDi 侧(bidi/Connection)的类共同复用 —— 两条协议通道共享同一套类型化事件基础设施,这也是 Puppeteer 跨浏览器架构能够统一 API 的原因之一。

六、总结:一类定义,处处通配

回到 关联文档,虽然它只呈现了一行类型定义与一个指向EventType的引用,但这行定义是整个 Puppeteer 类型化事件体系的关键枢纽:

  • 在类型层面,Events & { '*': Events[keyof Events] }在保留全部具体事件强类型的同时,追加了负载为联合类型的通配键,使EventEmitter、Page、bidi/Connection的on/off/once/emit/listenerCount/removeAllListeners都天然接受'*';
  • 在实现层面,EventEmitter.ts 的#handlers簿记以keyof Events | '*'为键,通配监听与普通监听在注册、计数、解绑、一次性触发与 dispose 清理上完全同构;
  • 在应用层面,PageEvents、BidiEvents等真实事件映射都经由它进入公开 API 签名,是阅读与二次开发 Puppeteer 事件系统时应当首先掌握的基础类型。

如果需要进一步阅读该体系下的相邻文档,可继续查阅 EventEmitter 类、CommonEventEmitter 接口、EventType 类型,以及各类的 Events 文档(如 PageEvents);类型本体与底层实现可对照 packages/puppeteer-core/src/common/EventEmitter.ts 源码阅读。

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询