行业资讯
📅 2026/9/7 3:50:42
Puppeteer CustomQueryHandler:注册自定义查询选择器扩展 page.$() 的完整解析
Puppeteer CustomQueryHandler注册自定义查询选择器扩展 page.$() 的完整解析【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer本文以 Puppeteer 官方 API 文档CustomQueryHandler接口为骨架结合仓库源码与测试用例完整讲解该接口的两个可选属性queryOne/queryAll、注册规则与命名限制、处理器如何被序列化注入浏览器页面、选择器前缀的解析优先级以及registerCustomQueryHandler、unregisterCustomQueryHandler、customQueryHandlerNames、clearCustomQueryHandlers四个配套 API。读完本文你将能够为 Puppeteer 编写自定义选择器引擎如按 id、按业务属性、按 Web Component 属性查询并理解其在 CSS/P 选择器体系中的位置与底层注入机制。一、CustomQueryHandler 接口两个可选的查询方法CustomQueryHandler是 Puppeteer 公开public的 TypeScript 接口定义在 CustomQueryHandler.ts 中export interface CustomQueryHandler { queryOne?: (node: Node, selector: string) Node | null; queryAll?: (node: Node, selector: string) IterableNode; }接口仅包含两个属性均为optional但注册时至少需要实现其中一个源码中有显式断言见下文属性修饰符类型说明queryOneoptional(node: Node, selector: string) Node \| null从node出发按给定selector搜索匹配的单个 DOMNode无匹配时返回nullqueryAlloptional(node: Node, selector: string) IterableNode从node出发按给定selector搜索匹配的多个Node返回可迭代对象两个方法的参数约定与浏览器原生的Node.querySelector/Node.querySelectorAll一致第一个参数node是查询的根节点。多数场景下它就是document但注意处理器也会被用在子元素查询上如ElementHandle.$会以该元素为根因此实现中不要默认node就是document除非你的选择器语义确实如此第二个参数selector是用户写在选择器前缀后面的那部分字符串例如注册名getById、使用page.$(getById/foo)时queryOne收到的selector就是foo。只实现一个方法时注入侧的注册器会自动做双向补齐在注入页面环境的 CustomQuerySelector.tsCustomQuerySelectorRegistry.register中若只提供了queryAll会用queryAll的第一个结果实现queryOne若只提供了queryOne会将其包装成只含 0 或 1 个元素的queryAll// 摘自 packages/puppeteer-core/src/injected/CustomQuerySelector.ts if (!handler.queryOne handler.queryAll) { const querySelectorAll handler.queryAll; handler.queryOne (node, selector) { for (const result of querySelectorAll(node, selector)) { return result; } return null; }; } else if (handler.queryOne !handler.queryAll) { const querySelector handler.queryOne; handler.queryAll (node, selector) { const result querySelector(node, selector); return result ? [result] : []; }; }也就是说只写queryOnepage.$$也能用最多返回一个元素只写queryAllpage.$也能用取第一个。二、注册Puppeteer.registerCustomQueryHandler 与命名规则接口本身只是“处理器定义”真正对外暴露的操作挂在Puppeteer类的静态方法上见 Puppeteer.tsstatic registerCustomQueryHandler( name: string, queryHandler: CustomQueryHandler, ): void { return this.customQueryHandlers.register(name, queryHandler); }注册后即可在任何接受选择器的位置使用name/selector或nameselector前缀官方注释给出的示例为import {Puppeteer} from puppeteer; Puppeteer.registerCustomQueryHandler(text, { /* … */ }); const aHandle await page.$(text/…);注册逻辑位于 CustomQueryHandler.ts 的CustomQueryHandlerRegistry.register约 L74–L126包含三条硬校验违反即抛错不允许覆盖已存在的注册名Cannot register over existing handler: ${name}。重复注册同名处理器必须先unregister或clear命名只允许大小写拉丁字母断言/^[a-zA-Z]$/.test(name)错误信息为Custom query handler names may only contain [a-zA-Z]。不允许数字、-、/、等特殊字符——这也是为什么 测试用例 中注册1/2/3会精确抛错至少实现一个查询方法At least one query method must be implemented.前缀分隔符与解析优先级选择器如何路由到自定义处理器由 GetQueryHandler.ts 中的getQueryHandlerAndSelector决定。其要点前缀分隔符有两类QUERY_SEPARATORS [, /]。即注册名getById后既可写getById/foo也可写getByIdfoo解析顺序是先自定义处理器、后内置处理器。内置查询处理器仅aria、pierce、xpath、text四种见BUILTIN_QUERY_HANDLERS它们与自定义处理器互不冲突——只要你的注册名不与之重名即可命中前缀后前缀会被剥离剩余部分作为selector传入queryOne/queryAll同时决定轮询策略自定义处理器与纯 CSS 一样默认使用PollingOptions.MUTATIONMutationObserver 轮询ARIA 类使用PollingOptions.RAF若选择器不带任何已注册前缀则进入 P 选择器解析::-p-xxx语法或回落到 CSS。从源码结构看自定义处理器被置于最高优先级这意味着你可以用Puppeteer.registerCustomQueryHandler(text, …)之类的方式“遮蔽”内置名——但注册阶段并不会阻止使用内置名遮蔽的实际效果发生在选择器解析阶段建议避免这种做法。三、底层机制处理器如何被注入到浏览器页面这是CustomQueryHandler最容易误解的一点queryOne/queryAll函数运行在浏览器页面里而不是 Node 侧。注册时 Puppeteer 会做两件事见register方法后半段用stringifyFunction把queryAll/queryOne序列化为函数字符串生成一段形如(PuppeteerUtil) { PuppeteerUtil.customQuerySelectors.register(getById, { queryAll: …, queryOne: … }); }的注册脚本将该脚本交给全局单例 scriptInjectorscriptInjector.append(registerScript)。ScriptInjector维护一个“补丁语句”集合在每次构造注入脚本时将 Puppeteer 内置的注入代码与所有自定义处理器注册脚本拼接在一起#get()方法// 摘自 packages/puppeteer-core/src/common/ScriptInjector.ts #get(): string { return (() { const module {}; ${injectedSource} ${[...this.#amendments] .map(statement { return (${statement})(module.exports.default);; }) .join()} return module.exports.default; })(); }之后QueryHandler在页面内执行查询时走的是页面环境中的PuppeteerUtil.customQuerySelectors.get(name)见 CustomQueryHandler.ts 中生成的querySelector/querySelectorAll静态方法再调用注册时的queryOne/queryAll。由这个机制可以推断出两条实践约束你的queryOne/queryOne实现里不能引用 Node 侧变量、外部导入或闭包外的复杂对象它会被toString()化后在浏览器中求值只能依赖 DOM API 与自身参数注销处理器时scriptInjector.pop(registerScript)会移除对应注册脚本保证后续构造的注入脚本不再包含该处理器已打开页面的存量注入不受影响新导航/新上下文才会生效。四、配套管理 APIPuppeteer类与CustomQueryHandler相关的静态方法共四个定义见 Puppeteer.tsAPI 文档分别为 registerCustomQueryHandler、unregisterCustomQueryHandler、customQueryHandlerNames、clearCustomQueryHandlers方法行为Puppeteer.registerCustomQueryHandler(name, handler)注册处理器校验命名与重复注册Puppeteer.unregisterCustomQueryHandler(name)按名注销名称不存在时抛Cannot unregister unknown handler: ${name}Puppeteer.customQueryHandlerNames()返回当前所有已注册处理器的名称数组Puppeteer.clearCustomQueryHandlers()注销全部自定义处理器逐个scriptInjector.pop后清空 Map注册、注销、命名校验的行为均有对应测试覆盖位于 elementhandle.test.ts 的Custom queries描述块约 L938 起其中每个用例后都调用Puppeteer.clearCustomQueryHandlers()防止相互污染。五、可运行的完整示例以下示例均取自仓库测试用例可直接改造使用。1. 只实现 queryOne按 id 查询import puppeteer from puppeteer; const browser await puppeteer.launch(); const page await (await browser.newPage()).setContent( div idnot-foo/div div idfoo/div, ); Puppeteer.registerCustomQueryHandler(getById, { queryOne: (_element, selector) { // 运行在浏览器页面内selector 为前缀后的字符串如 foo return document.querySelector([id${selector}]); }, }); // page.$ 使用 name/selector 前缀 const element (await page.$(getById/foo)) as ElementHandleHTMLDivElement; console.log(await page.evaluate(el el.id, element)); // foo console.log(Puppeteer.customQueryHandlerNames()); // [getById] // 注销后getById/foo 不再命中自定义处理器回落为普通选择器解析并报错 Puppeteer.unregisterCustomQueryHandler(getById);2. 只实现 queryAll按 class 查询多个元素await page.setContent( div idnot-foo/div div classfooFoo1/div div classfoo bazFoo2/div, ); Puppeteer.registerCustomQueryHandler(getByClass, { queryAll: (_element, selector) { return [...document.querySelectorAll(.${selector})]; }, }); // 只实现了 queryAllpage.$$ 直接可用 // 注入侧会自动用 queryAll 的第一个结果补齐 queryOnepage.$ 同样可用 const elements (await page.$$(getByClass/foo)) as ElementHandle[];3. 与 P 选择器P-selectors组合自定义处理器同样参与 P 选择器体系。queryhandler.test.ts约 L527 起中注册名为div的处理器后可用::-p-div前缀写法并支持参数形式Puppeteer.registerCustomQueryHandler(div, { queryOne(_, selector) { if (selector true) { return document.querySelector(div); } return document.querySelector(button); }, }); const a await page.$(::-p-div(true)); // 命中 div const b await page.$(::-p-div(true)); // 字符串参数形式同样命中 div const c await page.$(::-p-div); // 无参数时命中 button从测试看P 选择器形式对参数做了引号/字面量解析处理器收到的仍是解析后的字符串如true而name/selector前缀形式则不做这种参数解析直接把分隔符后的原文交给处理器。4. 非法命名会精确报错Puppeteer.registerCustomQueryHandler(1/2/3, { queryOne: () document.querySelector(foo), }); // 抛出Error: Custom query handler names may only contain [a-zA-Z]与 elementhandle.test.ts 中should throw with invalid query names用例断言完全一致。六、实践注意事项作用域是全局且按名称唯一注册表是 Node 侧单例CustomQueryHandler.ts 末尾的export const customQueryHandlers new CustomQueryHandlerRegistry()整个 Node 进程共享。测试代码普遍在afterEach中调用Puppeteer.clearCustomQueryHandlers()清理长驻进程如测试套件应遵循同样习惯函数体必须自包含如第三节所述函数会被字符串化后注入页面执行避免依赖 Node 侧闭包变量实现里只能操作node参数与 DOM重复注册先注销Cannot register over existing handler是断言错误而非警告若需要“更新”处理器先unregisterCustomQueryHandler再registerCustomQueryHandler前缀写法二选一name/selector与nameselector等价QUERY_SEPARATORS [, /]注意选择器内部若含/如某些 URL 型自定义语法只能使用自定义处理器自行解析框架不保证进一步切分与内置前缀的区别aria/、pierce/、xpath/、text/是内置解析器前缀解析优先级低于自定义名。注册名与内置名相同时可遮蔽内置行为属于源码结构上允许但不建议的用法。七、相关源码与文档索引接口定义与注册表CustomQueryHandler.tsCustomQueryHandlerL21–L30register校验 L74–L126页面注入侧注册器CustomQuerySelector.tsregister自动补齐 L25–L48注入脚本拼接ScriptInjector.ts#get()L40–L51公共静态 APIPuppeteer.tsL69–L95选择器路由与优先级GetQueryHandler.tsQUERY_SEPARATORSL25解析循环 L35–L55注册/注销/命名校验测试elementhandle.test.tsCustom queries块 L938 起与 P 选择器组合的测试queryhandler.test.tsL527–L594配套 API 文档registerCustomQueryHandler、unregisterCustomQueryHandler、customQueryHandlerNames、clearCustomQueryHandlers【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考