☰
axe-core 插件系统深度解析:基于跨域 iframe 的扩展机制与实例实现
2026/9/28 3:46:30 网站建设 项目流程
  • 测试

【免费下载链接】axe-core

Accessibility engine for automated Web UI testing

项目地址:https://gitcode.com/gh_mirrors/ax/axe-core
点击查看免费下载

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 ); } } ] });

这段代码揭示了插件系统的三个关键要素:

  1. 插件包含一个id。该 id 被用作访问插件及其实现的句柄。注册后可通过axe.plugins.doStuff访问到该插件对象。
  2. 插件通过axe.registerPlugin()注册到 axe(每个 iframe 内各自注册)。注意这里需要把 axe 库与插件注册脚本同时加载到每个 iframe 中,才能实现跨 frame 协作。
  3. 插件把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。其工作分为两段握手:

  1. ping 阶段:向 frame 的contentWindow发送axe.ping,默认等待 500ms(可通过options.pingWaitTime调整,设为 0 则跳过 ping)。若超时且未开启 debug 模式,则直接resolve(null)跳过该 frame——这样即使某些 iframe 未加载 axe,插件流程也不会被卡死。
  2. 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 实现,分两条路径并行:

  1. 本地清理:遍历axe.plugins,逐个调用Plugin.prototype.cleanup(其实现遍历_registry,依次调用每个实例的cleanup)。注意清理错误会被收集而不会阻断其他插件的清理。
  2. 跨 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

项目地址:https://gitcode.com/gh_mirrors/ax/axe-core
点击查看免费下载

相关推荐

上一篇:深入理解 Databasus 的 OpenSpec 技能体系:openspec-sync-specs 与 Agent 驱动的规格同步机制
下一篇:Wand-Enhancer 使用指南:免费解锁 WeMod Pro 功能与远程面板

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

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

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

立即咨询