ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

Puppeteer EventsWithWildcard 类型解析:为事件系统提供通配监听的类型基石 Puppeteer EventsWithWildcard 类型解析为事件系统提供通配监听的类型基石【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteerPuppeteer 的事件体系以 EventEmitter 为核心Page、Browser、BrowserContext、CDPSession、WebWorker等几乎所有关键类都继承自它并向外派发事件。本篇文章聚焦其中的一个基础类型 ——EventsWithWildcard说明它如何在不破坏每个事件强类型的前提下为监听所有事件这种需求提供编译期支持并结合仓库源码说明EventEmitter如何将通配键纳入真实的监听簿记以及Page等类的泛型方法如何依赖它。读完本文你将理解 Puppeteer 类型化事件映射Event Map的设计骨架并能正确编写通配事件处理代码。关联文档docs/api/puppeteer.eventswithwildcard.md一、类型定义EventsWithWildcard 究竟是什么Puppeteer 官方 API 文档给出了这个类型最精炼的定义export type EventsWithWildcardEvents extends RecordEventType, unknown Events { *: Events[keyof Events]; };其中EventType定义为string | symbol意味着一个事件键既可以是普通字符串例如request、close也可以是 Symbol相关定义见 docs/api/puppeteer.eventtype.md 与 EventEmitter 源码。这个类型做的事情可以拆成两步来理解约束输入泛型参数Events必须是一个以EventType为键的记录类型也就是 Puppeteer 各事件映射如PageEvents、BidiEvents的形态 —— 键是事件名值是事件负载类型。追加通配键把Events原样保留同时增加一个键*它的负载类型是Events[keyof Events]—— 也就是所有事件负载类型组成的联合类型。因此如果有一个自定义事件映射interface MyEvents { load: {url: string}; error: {code: number}; }那么EventsWithWildcardMyEvents展开后等效于{ load: {url: string}; error: {code: number}; *: {url: string} | {code: number}; }从语义上讲监听*意味着无论哪个事件发生都能收到回调而收到的负载不可能是某一个具体类型只可能是所有负载的联合 —— 因此联合类型是唯一正确的类型表达。这也是该类型名中WithWildcard带通配的由来。二、它为何存在类型化事件映射的补全在 EventEmitter 源码 中可以看到两个关键接口的配合关系// packages/puppeteer-core/src/common/EventEmitter.ts export interface CommonEventEmitterEvents extends RecordEventType, unknown { onKey extends keyof Events(type: Key, handler: HandlerEvents[Key]): this; offKey extends keyof Events(type: Key, handler?: HandlerEvents[Key]): this; emitKey extends keyof Events(type: Key, event: Events[Key]): boolean; onceKey extends keyof Events(type: Key, handler: HandlerEvents[Key]): this; listenerCount(event: keyof Events): number; removeAllListeners(event?: keyof Events): this; }而EventEmitter类在实现该接口时把所有方法签名中的Events都替换成了EventsWithWildcardEvents见 EventEmitter.ts 源码export class EventEmitter Events extends RecordEventType, unknown, implements CommonEventEmitterEventsWithWildcardEvents {implements泛型实例化使用的是EventsWithWildcardEvents于是类内所有方法的Key空间都被补全成了keyof Events | *。例如官方文档中的 EventEmitter.on() 签名即是onKey extends keyof EventsWithWildcardEvents( type: Key, handler: HandlerEventsWithWildcardEvents[Key], ): this;同理off、once、emit、listenerCount、removeAllListeners 全都以EventsWithWildcardEvents作为键空间来源。换句话说EventsWithWildcard是让 EventEmitter 全部 API 允许*键的单一开关—— 改动这一个类型所有方法的类型能力就同步更新这是这套设计的精妙之处。三、类型收益通配事件负载是联合类型把通配键接入之后实际开发中就能写出这样的监听代码page.on(*, event { // event 的编译期类型是 Page 全部事件负载的联合类型 // 运行时需要借助类型守卫进行收窄 if (typeof event string) { // 处理字符串类型的负载例如 frame 名称等 } else { // 处理对象类型的负载 } });注意这里*并不会干扰对其他具体事件的强类型由于EventsWithWildcardEvents Events { *: ... }on(load, handler)仍然精确地推导出handler的参数就是load事件的负载类型类型安全没有被破坏。通配键与具体键共存互不干扰。这正是交集类型 追加键这种写法的核心价值在不覆盖原事件映射任何键的前提下追加一个特殊键。由此也引出一个实用的编码建议监听*时编译器给到的是联合类型不要试图直接访问某一个具体负载才有的属性应当先做收窄typeof 判断、in操作符或自定义类型守卫再处理业务逻辑。而once(*, handler)语义同样成立 —— 只响应下一个任意事件。四、源码纵深EventEmitter 如何把 * 当作真实监听键如果只在类型层面做文章通配键并不能真正工作。让我们看 EventEmitter 类实现 的内部簿记结构#handlers new Mapkeyof Events | *, ArrayHandlerany();这是一个以事件键为键的处理器数组映射而键的联合类型明确包含*。也就是说在实现层面*与任何具体事件名地位完全平等 —— 向on(*, handler)注册时onKey extends keyof EventsWithWildcardEvents(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 emitterEventEmitter 默认以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 Pageoverride on/off 时的键空间Page.ts 在第 50 行导入了EventsWithWildcard随后重写了on与off// packages/puppeteer-core/src/api/Page.ts override onK extends keyof EventsWithWildcardPageEvents( type: K, handler: (event: EventsWithWildcardPageEvents[K]) void, ): this { ... } override offK extends keyof EventsWithWildcardPageEvents( type: K, handler: (event: EventsWithWildcardPageEvents[K]) void, ): this { ... }Page的PageEvents事件映射覆盖了请求/响应、导航、对话框、Worker、截图等全部页面事件事件常量见 docs/api/puppeteer.pageevent.md完整事件枚举见 docs/api/puppeteer.pageevents.md。通过把PageEvents交给EventsWithWildcardpage.on/page.off既保留了针对PageEvent.Request这类具体键的精确推导又把*纳入合法的 type 参数。Page之所以要 override 这两个方法是因为它对Request事件做了特殊接线为了协作式请求拦截监听会被包装进enqueueInterceptAction回调见 Page.ts on 实现而off时需要还原出原始 handler 才能正确解绑Page.ts off 实现—— 泛型签名中保留EventsWithWildcardPageEvents正是为了让这种包装逻辑对普通键与通配键都保持一致的类型体验。5.2 WebDriver BiDi 连接override emit 时的键空间在 WebDriver BiDi 协议连接中bidi/Connection.ts 同样从../common/EventEmitter.js导入了该类型第 14 行并以此重写emitoverride emitKey extends keyof EventsWithWildcardBidiEvents( key: Key, event: EventsWithWildcardBidiEvents[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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进