Storybook a11y 插件深度解析:基于 axe-core 的无障碍测试与面板实践
@storybook/addon-a11y 是 Storybook 官方提供的无障碍(Accessibility,简称 a11y)测试插件,它基于 axe-core 引擎在 Story 渲染完成后自动执行 WCAG 规则检查,并在 Manager 面板中展示违规详情与视觉模拟结果。本文以 a11y 插件 README 为核心骨架,结合插件源码(code/addons/a11y/src)逐一拆解其安装机制、a11y 参数体系、检查执行链路与结果呈现,帮助你既会用这个插件,又理解它"为什么这样跑"。
一、插件定位与安装方式
README 对插件的定位非常简洁:为 Storybook stories 提供无障碍测试,底层使用 axe-core 执行检查。从 package.json 可以确认其关键事实:
- 运行时依赖仅为
axe-core(^4.2.0)与@storybook/global,插件本体非常轻; - 包名描述明确为 "Test UI component compliance with WCAG web accessibility standards";
storybook.unsupportedFrameworks声明了react-native不受支持——无障碍检查依赖真实 DOM,React Native 没有浏览器 DOM,因此被显式排除;exports暴露了四个入口:.(preview 侧装饰器与类型)、./manager、./preset、./preview,对应 Storybook addon 的 Manager/Preview 双端模型。
在已有 Storybook 中启用
官方推荐的启用方式是一条命令:
npx storybook add @storybook/addon-a11y
这条命令背后做了两件事,源码可以直接印证:
- 注册 preset。preset.ts 只导出一个布尔值
isAddonA11yEnabled = true,其注释写明用途:让其他 addon 或 preset 通过await presets.apply('isAddonA11yEnabled', false)探测 a11y 是否启用,从而调整自身行为——这是一种轻量级的"addon 间能力协商"机制。 - 执行 postinstall 迁移脚本。postinstall.ts 会在安装时调用子命令:
const args = [
useRemotePkg ? `storybook@${versions.storybook}` : `storybook`,
'automigrate',
'addon-a11y-addon-test',
'--loglevel', 'silent',
'--skip-doctor',
];
即自动运行 storybook automigrate addon-a11y-addon-test 迁移器。它支持透传 --yes、--package-manager、--config-dir 等选项,并用 stdio: ['ignore', 'pipe', 'pipe'] 处理 stdin——源码注释特别解释了必须把 stdin 设为 ignore,否则嵌套的 CLI 收不到 EOF 会永久阻塞外层 add 命令。这说明插件安装不只是"加个包",还会对既有配置做自动化改造。
二、架构总览:Preview 端跑检查,Manager 端看结果
从源码结构看,a11y 插件遵循 Storybook addon 的标准双端模型:
code/addons/a11y/
├── src/
│ ├── preview.tsx # Preview 端:afterEach 钩子,自动触发检查
│ ├── manager.tsx # Manager 端:注册面板与视觉模拟工具
│ ├── a11yRunner.ts # 核心:封装 axe-core 的执行器
│ ├── a11yRunnerUtils.ts # 结果增强:生成可定位的 linkPath
│ ├── params.ts # a11y 参数类型定义
│ ├── types.ts # 全局参数、结果类型、状态机
│ ├── constants.ts # ADDON_ID / 事件名 / 面板 ID
│ ├── AccessibilityRuleMaps.ts # axe 规则 → 友好文案映射
│ ├── postinstall.ts # 安装期迁移
│ ├── withVisionSimulator.ts # 色觉障碍视觉模拟装饰器
│ └── components/ # A11YPanel / Report / VisionSimulator 等 UI
└── template/stories/ # 官方示例 stories(参数用法演示)
两端通过 addons channel 通信,constants.ts 定义了事件总线:
| 事件/标识 | 值 | 作用 |
|---|---|---|
ADDON_ID |
storybook/a11y |
addon 注册 ID |
PANEL_ID |
storybook/a11y/panel |
结果面板 ID |
PARAM_KEY |
a11y |
面板关联的 parameters 键 |
EVENTS.RESULT / ERROR / MANUAL |
storybook/a11y/... |
preview → manager 的结果回传 |
Manager 端 manager.tsx 注册了两个 addon:一个是 types.PANEL 类型的结果面板(标题动态显示 "Accessibility" 与违规计数 Badge),一个是 types.TOOL 类型的视觉模拟工具(VisionSimulator)。面板仅在 viewMode === 'story' 时匹配——Docs 视图下不出现,这与检查只在 story 渲染后触发相呼应。
三、a11y 参数体系:types 与 defaults 一览
插件对外的配置面集中在 params.ts 与 types.ts。完整参数类型如下:
// code/addons/a11y/src/params.ts
type A11yTest = 'off' | 'todo' | 'error';
export interface A11yParameters {
/** axe-core run 的 Context 参数(不支持直接传 Node/NodeList) */
context?: ContextSpecWithoutNode;
/** axe-core 运行选项(对应 RunOptions) */
options?: RunOptions;
/** axe-core 配置对象(对应 Spec) */
config?: Spec;
/** 是否禁用无障碍测试 */
disable?: boolean;
/** 违规处理方式:'off' | 'todo' | 'error' */
test?: A11yTest;
}
全局参数(Globals)则只有一个开关:
// code/addons/a11y/src/types.ts
export interface A11yGlobals {
a11y?: {
/** 访问 story 时不自动执行检查(仍可手动在面板触发) */
manual?: boolean;
};
}
默认值由 preview.tsx 中的 initialGlobals 与 parameters 声明:
export const initialGlobals = {
a11y: { manual: false },
vision: undefined,
};
export const parameters = {
a11y: { test: 'todo' }, // 默认将违规报告为 warning
};
各参数语义归纳:
| 参数 | 位置 | 作用 | 默认值 |
|---|---|---|---|
test: 'off' |
parameters / meta / story | 完全关闭该 story 的自动化检查 | 无(插件级默认 'todo') |
test: 'todo' |
同上 | 有违规时按 warning 报告,不视为失败 | 'todo' |
test: 'error' |
同上 | 有违规时按 failed 报告 | 无 |
disable: true |
同上 | 禁用检查(与 test: 'off' 等效的另一种写法) |
false |
context |
同上 | 限定检查范围,支持选择器、选择器数组或 { include, exclude } 对象 |
document.body 减去 Storybook 内部元素 |
options |
同上 | 透传 axe-core RunOptions(如 runOnly、checks) |
{} |
config |
同上 | 透传 axe-core Spec(自定义/禁用规则等) |
{} |
a11y.manual |
globals(URL/预览配置) | 关闭"访问 story 即自动检查",改为面板手动触发 | false |
值得注意:源码中 context 类型是 ContextSpecWithoutNode——即刻意剥离了 Node / NodeList 支持。运行器会显式拒绝 element 参数并抛出 ElementA11yParameterError(见 a11yRunner.ts 中 if (input.element) throw ...),从结构上强制使用可序列化选择器,保证参数可被 URL 编码与持久化。
四、执行链路:afterEach → 串行队列 → channel 回传
4.1 何时跑检查
preview.tsx 导出的是 preview 侧的 afterEach 钩子,判定逻辑完整还原如下:
const shouldRunEnvironmentIndependent =
!isGhostStories && // ghost stories 场景跳过
a11yParameter?.disable !== true && // parameters.a11y.disable
a11yParameter?.test !== 'off' && // parameters.a11y.test
a11yGlobals?.manual !== true; // globals.a11y.manual
if (shouldRunEnvironmentIndependent && viewMode === 'story') {
const result = await run(a11yParameter, storyId);
const hasViolations = (result?.violations.length ?? 0) > 0;
reporting.addReport({
type: 'a11y',
version: 1,
result,
status: hasViolations ? getMode() : 'passed',
});
}
要点:
- 检查只在
viewMode === 'story'时触发,Docs 视图不跑; - 参数可按 parameters 继承链在 preview(全局)、meta、story 三级配置,story 级
disable/off可覆盖全局; - 结果统一写入 Storybook 的 reporting 体系(
type: 'a11y'),这正是它能在 Manager 面板与组件测试报告中被统一消费的原因; - 违规时的 status 由
test参数决定:'todo'→warning,'error'→failed。
4.2 a11yRunner:为什么用串行队列
a11yRunner.ts 是整个插件的核心。它的 run() 函数有几个关键设计:
1. 串行队列。 源码注释写明 "axe-core is not designed to run in parallel",因此所有 axe.run 调用被推入一个 promise 队列,由 runNext() 逐个消费。这意味着快速切换多个 story 时检查会排队执行,而不是并发竞争 axe 内部状态。
2. 默认检查范围排除 Storybook 内部元素:
const context: ContextSpec = {
include: document?.body,
exclude: ['.sb-wrapper', '#storybook-docs', '#storybook-highlights-root'],
};
.sb-wrapper、Docs 容器与高亮根节点都属于 Storybook 框架自身 DOM,不检查它们可以避免框架层面的误报。用户传入的 context.exclude 会追加到这个默认列表,而 context.include 会直接替换默认 include——模板示例 stories 里的 SingleNodeContext / MultipleNodeContext / IncludeAndExcludeContext 正好演示了这三种写法:
// 单选择器
a11y: { context: '#custom-target' }
// 多选择器
a11y: { context: ['#first-custom-target', '#second-custom-target'] }
// include + exclude
a11y: {
context: {
include: ['#parent-node'],
exclude: ['#second-custom-target'],
},
}
IncludeAndExcludeFromShadowDOMContext 还演示了 Shadow DOM 场景下 fromShadowDom 的 include/exclude 用法,并在 play 函数中动态 attachShadow 注入测试内容。
3. 默认禁用 region 规则。
const DISABLED_RULES = [
// In component testing, landmarks are not always present
// and the rule check can cause false positives
'region',
] as const;
组件级测试中通常没有完整的页面 landmark 结构,region 规则(要求所有内容被 landmark 包裹)会产生大量误报,所以插件在 axe.configure 前统一将其禁用,且用户可通过 config.rules 显式重新启用(规则按顺序应用,后者覆盖前者)。
4. runOnly 与 rules 的合并修正。 mergeDisabledRulesIntoRunOptions 处理一个 axe-core 的边界情况:axe.run({ runOnly }) 可能重新启用被 config.rules 禁用的带 tag 规则,因此当存在 runOnly 时,插件会把 config.rules 中的禁用项镜像进 options.rules(且用户自己的 options.rules 优先级最高),并且不修改用户传入的原始对象。
5. 结果回传与 CSP 兼容。 MANUAL 事件处理先 await waitForAnimations(),再对结果做一次 JSON.parse(JSON.stringify(result))——源码注释解释:axe 结果中包含类实例,直接经 telejson 反序列化会违反 Manager 侧 CSP(script-src),所以用深拷贝"抹平"类实例。
4.3 linkPath:每个违规节点都能一键定位
a11yRunnerUtils.ts 的 withLinkPaths 会为 passes / incomplete / violations 中的每个 node 生成:
const id = `${key}.${result.id}.${index + 1}`;
const linkPath = `${pathname}?path=/story/${storyId}&addonPanel=${PANEL_ID}&a11ySelection=${id}`;
即形如 ?path=/story/button--primary&addonPanel=storybook/a11y/panel&a11ySelection=violations.color-contrast.1 的深链——这正是"测试报告中的某条违规"能直接跳转到面板并高亮对应 DOM 节点的机制,也让 a11y 报告可以被外部工具(如 CI 汇总页)以 URL 形式引用。
五、测试语义:'todo' 与 'error' 在两种运行环境下的行为
test 参数同时影响 UI 报告状态与测试运行结果:
- Storybook UI 环境:
afterEach通过reporting.addReport上报 status('todo'→warning,'error'→failed),面板与状态徽标据此显示; - Vitest 独立运行环境(portable stories 场景):preview.tsx 会检测
getIsVitestStandaloneRun(),当存在违规且 mode 为failed时,动态加载vitest-axe/matchers的toHaveNoViolations并执行expect(result).toHaveNoViolations()使测试真正失败;运行出错时则直接throw e让 vitest 捕获。源码注释将其标注为过渡方案(todo:未来统一由 portable stories 层级处理异常)。
这套双环境语义意味着:把 story 的 a11y 检查接入 test('error') 后,同一条 story 文件既能被 Storybook UI 消费,也能在纯 vitest 下作为失败门禁运行。
六、规则映射:从 axe 规则 ID 到"人话"
面板不只展示 axe-core 原始输出。AccessibilityRuleMaps.ts 维护了一份大型映射表 combinedRulesMap,将每条 axe 规则 ID 映射为三元组:
export type AxeRuleMap = {
[axeId: string]: {
title: string; // 规则简名
axeSummary: string; // axe-core 官方摘要
friendlySummary: string; // 面向使用者的修复建议
};
};
映射按合规来源分层组织:
| 分表 | 覆盖范围 | 代表规则 |
|---|---|---|
axeRuleMapping_wcag_2_0_a_aa |
WCAG 2.0 A/AA | color-contrast、image-alt、link-name、html-has-lang、button-name |
axeRuleMapping_wcag_2_1_a_aa |
WCAG 2.1 A/AA | autocomplete-valid、avoid-inline-spacing |
axeRuleMapping_wcag_2_2_a_aa |
WCAG 2.2 A/AA | target-size(触控目标尺寸) |
axeRuleMapping_wcag_2_x_aaa |
WCAG AAA(默认不跑) | color-contrast-enhanced、identical-links-same-purpose |
axeRuleMapping_best_practices |
行业最佳实践 | heading-order、skip-link、tabindex、region、empty-heading |
axeRuleMapping_experimental |
实验性(默认禁用) | p-as-heading、hidden-content |
axeRuleMapping_deprecated |
已弃用 | aria-roledescription |
这种分层解释了面板中"每条违规同时显示技术描述与修复建议"的体验:axeSummary 回答"违反了什么规范",friendlySummary 回答"我应该怎么改"。
七、可选进阶配置示例
以下示例全部来自仓库内的官方模板与文档片段,可直接参考。
7.1 调整运行选项:关闭单项 check 或启用 AAA 规则集
模板示例 中的 Options story 展示如何在 story 级禁用颜色对比检查:
export const Options = {
args: {
content: '<button style="color: rgb(255, 255, 255); background-color: rgb(76, 175, 80);">Click me!</button>',
},
parameters: {
a11y: {
config: {},
options: {
checks: {
'color-contrast': { enabled: false },
},
},
},
},
};
Config story 则展示用 config.rules + disableOtherRules 只跑单条规则(如 avoid-inline-spacing)。
在 preview 全局启用 WCAG AAA 规则,参考 文档片段(注意必须显式重列默认 tag,再追加 wcag2aaa):
const preview: Preview = {
parameters: {
a11y: {
options: {
runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice', 'wcag2aaa'],
},
},
},
};
7.2 关闭检查的两种方式
// 方式一:story/meta 级
export const Disabled = {
parameters: { a11y: { disable: true } },
};
// 方式二:story 级把违规降级/关闭
parameters: { a11y: { test: 'off' } } // 或 'todo' / 'error'
7.3 手动模式
在 globals 中设置 a11y: { manual: true } 后,访问 story 不再自动执行检查,但仍可在 Accessibility 面板中手动触发(EVENTS.MANUAL 通道即为此设计,触发前会 waitForAnimations() 等待动画稳定)。
八、视觉模拟:色觉障碍模拟面板
除了"查违规",该插件还内置一个辅助理解问题的工具:色觉模拟。withVisionSimulator.ts 作为 preview 端装饰器工作:
- 读取
globals.vision(初始值undefined,即不应用任何滤镜); - 将 visionSimulatorFilters.ts 中定义的 CSS
filter应用到document.body,并与既有 filter 字符串做正则替换、拼接/还原,避免覆盖用户自定义 filter; - 用
MutationObserver监听 body class 变化(Storybook 切换渲染区时会改 class),保证滤镜状态在 story 切换后仍正确; - 滤镜的 SVG 定义以
filterDefs形式注入 body,卸载时清理。
Manager 侧的 VisionSimulator 工具组件(见 components/VisionSimulator.tsx)提供切换入口。典型用途:当 color-contrast 违规报告出现时,用模拟视图直观感受"对比度不足对用户意味着什么"。
九、如何读面板与结果
结合 manager.tsx 与 types.ts 可以确认面板的信息组织:
- 标题 Badge 计数 =
violations.length + incomplete.length(违规 + 不确定项); - 结果分为三类 tab,对应
RuleType常量:violations/incomplete/passes,默认停留在 violations; - 面板状态机
Status覆盖initial | manual | running | error | component-test-error | ran | ready,因此"检查中/失败/完成"都有明确 UI 状态,不会静默; - 每条违规节点携带第四节所述的
linkPath,可一键深链; - 跨环境结果不一致时的处理思路,插件在 constants.ts 中内置了对官方文档锚点
why-are-my-tests-failing-in-different-environments的跳转链接(DOCUMENTATION_DISCREPANCY_LINK),对应仓库内 写作测试文档。
十、关键文件索引
| 关注点 | 文件 |
|---|---|
| 插件说明与安装命令 | README.md |
| 参数类型定义 | params.ts / types.ts |
| 自动检查钩子(afterEach) | preview.tsx |
| axe-core 执行器与队列 | a11yRunner.ts |
| 结果深链增强 | a11yRunnerUtils.ts |
| 面板与工具注册 | manager.tsx |
| 规则友好文案映射 | AccessibilityRuleMaps.ts |
| 安装期迁移脚本 | postinstall.ts |
| 官方参数示例 stories | template/stories/parameters.stories.ts / tests.stories.ts |
| 单元测试(执行器行为验证) | a11yRunner.test.ts / preview.test.tsx |
| 官方 a11y 参数文档片段 | docs/_snippets/ 下的 addon-a11y-* 系列 |
适用前提说明:本文结论基于当前仓库(a11y 包版本 10.6.0-beta.1)的源码状态;axe-core 依赖声明为 ^4.2.0,具体规则集行为以所安装 axe-core 版本为准。React Native 框架下该 addon 不可用,这是 package.json 中显式声明的限制而非偶然缺失。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00