- 测试
【免费下载链接】axe-core
Accessibility engine for automated Web UI testing
axe-core(Accessibility engine for automated Web UI testing)在核心自动化无障碍审计能力之外,提供了一套通用插件系统。它利用 axe 的跨域 iframe 通信能力,允许开发者在不改动审计内核的前提下扩展出元素高亮、人工辅助审计等定制功能。阅读本文后,你将掌握axe.registerPlugin()、axe.plugins、sendCommandToFrame与axe.cleanup()的完整协作模型,并能够基于文档与源码实现自己的插件和插件实例。
插件系统是什么:一套"工具注册表"
axe 的插件系统是一套**通用目的(general purpose)**的扩展机制,其核心价值在于:它复用了 axe 既有的跨域 iframe 能力,把"在每个 iframe 中执行自定义逻辑"这件事抽象成标准化的注册与调用流程。插件系统最初是为了支撑元素高亮(highlighting of elements)这类功能而设计,后来也被广泛用于人工无障碍审计(manual accessibility auditing)等各类辅助工具。
从概念上看,插件(plugin)可以视为一个工具注册表(a registry of tools):插件本身先注册到 axe,然后允许插件实例(plugin instance)再注册到插件上。这种两级注册结构把"能力定义"与"具体实现"解耦——插件定义一种可执行的行为模式,而实例提供该模式的具体动作。在核心代码中,这一设计体现在 lib/core/public/plugins.js 的Plugin类上:Plugin构造器接收spec,其中包含_run、_collect与commands,实例则被存放在_registry对象中。
一个简单的 "act" 插件:run 函数与命令机制
插件目前支持两个函数:run函数和**collect函数**。两者可以组合使用,在 axe 系统之上实现复杂行为。文档以doStuff插件为例,展示一个在每个 iframe 内执行某种动作的 "act" 插件——例如实现一个高亮页面中某一类元素的实例。
创建插件需要实现run函数,以及用于在页面每个含 axe 的 iframe 内注册并执行该run函数的命令。一个 noop 版本的最小实现如下(该示例在 doc/plugins.md 与集成测试 test/integration/full/plugin/plugin.js 中均有完整呈现):
axe.registerPlugin({ id: 'doStuff', run: function (id, action, options, callback) { var frames; var q = axe.utils.queue(); var that = this; frames = axe.utils.toArray(document.querySelectorAll('iframe, frame')); frames.forEach(function (frame) { q.defer(function (done) { axe.utils.sendCommandToFrame( frame, { options: options, command: 'run-doStuff', parameter: id, action: action }, function () { done(); } ); }); }); if (!options.context.length) { q.defer(function (done) { that._registry[id][action].call( that._registry[id], document, options, done ); }); } q.then(callback); }, commands: [ { id: 'run-doStuff', callback: function (data, callback) { return axe.plugins.doStuff.run( data.parameter, data.action, data.options, callback ); } } ] });这段代码揭示了插件系统的三个关键要素:
- 插件包含一个
id。该 id 被用作访问插件及其实现的句柄。注册后可通过axe.plugins.doStuff访问到该插件对象。 - 插件通过
axe.registerPlugin()注册到 axe(每个 iframe 内各自注册)。注意这里需要把 axe 库与插件注册脚本同时加载到每个 iframe 中,才能实现跨 frame 协作。 - 插件把
run函数与commands一起注册进 axe 系统。这一方面让插件实例可以被注册到插件上并被执行;另一方面在每个 iframe 内注册了各命令的处理器,使插件能够跨越 iframe 边界与自身协调。
run 函数的执行流程
当调用方想调用某个插件实例时,它会在顶层文档调用插件的run函数,并传入:要调用的插件实例 id、要调用的实例动作(action)、options 以及回调函数。
run函数随后通过axe.utils.sendCommandToFrame()把同样的指令发送到每个 iframe 中该插件的实现。等待 iframe 内命令完成后,再在当前文档内执行本实例的动作函数(当options.context为空时,作用于整个document)。在上述实现中,axe.utils.queue()这个 promise 工具被用来协调跨 iframe 通信的异步处理。
命令处理器(command handler)的回调会在每个 iframe 内再次调用插件的run函数——这本质上构成了一次对run函数的递归调用:每个 iframe 收到命令后继续向自己的子 iframe 下发,直到叶子 frame 执行完毕。当所有 iframe 的run函数都执行完成后,回调被逐级触发,相当于沿 iframe 层级递归"返回",最终在顶层文档执行真正的回调。这种机制也可以被用来把数据从 iframe 层级中向上回传(即 collect 类场景的基础,属于更进阶的话题)。
命令的底层注册:audit.registerCommand
命令(command)是如何被 axe 记住的?在 lib/core/public/plugins.js 的Plugin构造器中:
function Plugin(spec) { this._run = spec.run; this._collect = spec.collect; this._registry = {}; spec.commands.forEach(command => { axe._audit.registerCommand(command); }); }每个命令都被交给axe._audit.registerCommand()存入审计对象的命令表。其实现位于 lib/core/base/audit.js:
registerCommand(command) { this.commands[command.id] = command.callback; }而命令的分发入口在 lib/core/public/load.js 的runCommand中:当 iframe 内收到axe.start消息时,除了内置的rules、cleanup-plugin等命令外,会查找axe._audit.commands[data.command]并调用对应回调——这正是run-doStuff这类自定义命令得以执行的通道:
default: if ( axe._audit && axe._audit.commands && axe._audit.commands[data.command] ) { return axe._audit.commandsdata.command; }插件实例:注册具体动作
插件的run只是"调度框架",真正做事的动作由**插件实例(plugin instance)**提供。下面实现一个doStuff的 "highlight" 实例——在每个 iframe 中给匹配选择器的元素加上一个基础边框(示例中实际的高亮代码被有意省略,留给读者自行实现):
var highlight = { id: 'highlight', highlighter: new Highlighter(), run: function (contextNode, options, done) { var that = this; Array.prototype.slice .call(contextNode.querySelectorAll(options.selector)) .forEach(function (node) { that.highlighter.highlight(node, options); }); done(); }, cleanup: function (done) { this.highlighter.clear(); done(); } }; axe.plugins.doStuff.add(highlight);观察这段代码可以得出插件实例的完整形态:
- 实例有一个
id('highlight'),用于被插件寻址; - 实例有一个
cleanup函数,负责撤销实例产生的副作用; - 实例可以包含任意数量的私有成员或动作成员(如这里的
highlighter对象); add()函数由 axe 自动提供,我们无需自己实现。在 lib/core/public/plugins.js 中,add的实现就是把实例按 id 存入插件的注册表:
Plugin.prototype.add = function add(impl) { this._registry[impl.id] = impl; };这里实例的动作名为run,因此注册完成后,在页面顶层 iframe 中可以这样调用:
axe.plugins.doStuff.run('highlight', 'run', options, callback);其中options需要包含selector(如'.my-heading')与context(空数组表示作用于整个 document)。此时顶层的run会把指令广播到所有 iframe,每个 iframe 内的run-doStuff命令又递归调用本 frame 的run('highlight', 'run', ...),最终在每个 frame 的 document 上执行highlight.run,完成全页面范围内的元素高亮。
用源码印证调用链
集成测试 test/integration/full/plugin/plugin.js 完整复现了上述链路并给出了可运行的断言:
axe.registerPlugin({...})之后assert.isOk(axe.plugins.doStuff),验证插件挂载到axe.plugins命名空间;axe.plugins.doStuff.add(highlight)之后assert.equal(axe.plugins.doStuff._registry.highlight, highlight),验证实例进入注册表;- 调用
axe.plugins.doStuff.run('highlight', 'run', { selector: '.my-heading', context: [] }, ...)后断言元素背景色变为黄色,验证端到端执行; - 调用
axe.cleanup()后断言背景色恢复,验证清理语义。
axe.plugins与axe.registerPlugin的挂载点定义在核心入口 lib/core/core.js:
axe.plugins = {}; axe.registerPlugin = registerPlugin;跨 iframe 通信:sendCommandToFrame 的底层协议
插件系统的根基是axe.utils.sendCommandToFrame(),它的完整实现位于 lib/core/utils/send-command-to-frame.js。其工作分为两段握手:
- ping 阶段:向 frame 的
contentWindow发送axe.ping,默认等待 500ms(可通过options.pingWaitTime调整,设为 0 则跳过 ping)。若超时且未开启 debug 模式,则直接resolve(null)跳过该 frame——这样即使某些 iframe 未加载 axe,插件流程也不会被卡死。 - start 阶段:ping 成功后发送
axe.start并携带参数(command、parameter、action、options等),默认等待 60 秒(可通过options.frameWaitTime调整),frame 内由respondable.subscribe('axe.start', runCommand)接收并分发(见 lib/core/public/load.js)。
两个超时参数(pingWaitTime、frameWaitTime)都来自调用方传入的options,这解释了插件run函数把options原样塞进命令参数的原因——它不仅承载业务配置(如selector),也控制着通信层的容错行为。
异步协调:axe.utils.queue()
run函数中把对每个 iframe 的调用以及本地文档内的执行全部q.defer(...)进队列,最后统一q.then(callback),其实现见 lib/core/utils/queue.js。queue 是一个"并行发起、统一收敛"的异步队列:
defer(fn)把一个任务压入任务列表并立即开始执行(pop()遍历尚未开始的任务);- 每个任务通过
createResolve标记完成,remaining计数递减; - 当所有任务完成时,
then注册的完成回调被调用一次,并收到各任务的结果数组; - 若某任务抛错,队列会进入
abort/catch路径,避免回调悬挂。
这正对应插件文档中所说的"使用 axe promise 工具axe.utils.queue()协调跨 iframe 的异步通信"——所有 frame 的回复与本地动作完成之前,顶层回调不会被触发。
插件清理:axe.cleanup() 的级联语义
文档强调:所有插件实例的 cleanup 函数会在调用axe.cleanup()时被执行,且该函数会自动调用页面所有 iframe 中所有插件实例的 cleanup 函数。这一语义由 lib/core/public/cleanup.js 实现,分两条路径并行:
- 本地清理:遍历
axe.plugins,逐个调用Plugin.prototype.cleanup(其实现遍历_registry,依次调用每个实例的cleanup)。注意清理错误会被收集而不会阻断其他插件的清理。 - 跨 frame 清理:通过
axe.utils.getFlattenedTree(document.body)找到所有iframe, frame,向其发送command: 'cleanup-plugin';frame 内的runCommand命中case 'cleanup-plugin'后递归调用本地的axe.cleanup()(见 lib/core/public/load.js),从而形成沿 iframe 树逐级下发的清理级联。
两条路径都汇入同一个 queue,全部完成后统一 resolve。对插件作者而言这意味着:只要在每个实例上实现cleanup(如恢复元素样式、移除注入节点、断开监听器),并确保 axe 与插件脚本在每个 iframe 中都已加载,axe.cleanup()一次调用即可完成整页状态的回收,无需自己维护 frame 清单。
实践要点与边界
- 每个 iframe 都要加载 axe 与插件:插件命令依赖 iframe 内存在可响应的 axe 实例;未加载 axe 的 frame 会被 ping 超时机制跳过(默认 500ms),因此插件只作用于已注入 axe 的 frame。
- 实例动作是约定而非强制:
run的调度基于that._registry[id][action],动作名由插件约定(示例中为run),你也可以按需实现collect等动作,只要顶层调用与命令回调的传参保持一致。 - 上下文控制:
options.context非空时,本地 document 的动作将被跳过(仅执行 iframe 内的分发),可据此实现"只作用于指定 iframe"的精细控制。 - 清理务必配对:任何产生可见副作用(样式、DOM、全局监听)的实例都应实现
cleanup,否则重复运行可能造成状态污染;测试 test/core/public/plugins.js 与 test/core/public/cleanup.js 对此有独立覆盖。
小结
axe-core 的插件系统通过"插件注册(lib/core/public/plugins.js)→ 命令登记(lib/core/base/audit.js)→ iframe 命令分发(lib/core/public/load.js)→ 跨 frame 通信(lib/core/utils/send-command-to-frame.js)→ 队列收敛(lib/core/utils/queue.js)→ 级联清理(lib/core/public/cleanup.js)"这一完整链路,把"在每个 iframe 中执行自定义动作"标准化为可组合、可递归、可回收的编程模型。无论你要做元素高亮、人工审计辅助工具,还是跨 frame 的数据汇总,都可以按本文的registerPlugin+ 实例add模式快速落地,并以集成测试 test/integration/full/plugin/plugin.js 为模板验证你的实现。
- 测试
【免费下载链接】axe-core
Accessibility engine for automated Web UI testing
相关推荐
Canary Server插件系统:Plugin扩展机制深度解析
Canary Server插件系统:Plugin扩展机制深度解析 概述 Canary Server作为OpenTibia社区的主流服务器模拟器,其插件系统(Pl
游戏开发后端网络终极指南:如何快速掌握Kibana插件系统与扩展机制
终极指南:如何快速掌握Kibana插件系统与扩展机制 Kibana作为Elastic Stack的核心可视化平台,其强大的插件系统与扩展机制是实现个性化功能的关
前端数据可视化数据分析后端可观测性Chisel命令扩展机制:插件系统的设计与实现
Chisel命令扩展机制:插件系统的设计与实现 引言 在iOS应用调试过程中,开发者常常需要面对复杂的视图层级、难以定位的性能问题以及繁琐的调试流程。传统的LL
开发工具调试器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考