Lighthouse 的 axe 检查仅静态扫描,无法捕获 JS 动态插入 DOM、运行时状态变更(如 aria-expanded 切换)、SSR 与 hydration 不一致导致的语义丢失,故不能单靠其跑分。

为什么不能只靠Lighthouse跑分
Lighthouse 的 axe 检查只是静态扫描,它看不到 JS 动态插入的 DOM、运行时状态变更(比如 aria-expanded 切换后是否同步更新)、或 SSR 与客户端 hydration 不一致导致的语义丢失。很多项目在 Lighthouse 得分 95+,但屏幕阅读器实际读不出 fieldset 里的 legend,原因就是 JS 覆盖了原生结构。
常见错误现象:
-
document.getElementById("main").innerHTML = "<nav>...</nav>"替换内容后,nav失去语义上下文 - React 组件中用
div渲染 tab 列表,但没加role="tablist"或漏掉aria-controls - 表单提交后 JS 清空并重建
input,但新节点没重新绑定id和for
用 gumbo-parser 做 HTML 结构快照比对
健壮性测试的核心不是“有没有 ARIA”,而是“HTML 是否始终符合解析预期”。gumbo-parser 是少数能严格按 HTML5 规范解析并暴露 DOM 树结构的 C 库,适合做 baseline 验证。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 在 CI 中对关键页面(如登录页、结账流程)生成服务端渲染后的 HTML 快照,用
gumbo_parse解析,断言output->root->children中必须存在nav、main、form等语义节点 - 检查每个
input节点是否都有id属性,且该id在文档中唯一(gumbo_get_attribute可提取) - 对比前后两次快照:若某次构建后
fieldset节点数从 3 变成 0,说明 JS 逻辑意外移除了语义容器,立即阻断发布
给自动化测试注入可访问性断言
不要把可访问性测试写成独立脚本——它必须嵌入现有 E2E 流程。Playwright / Cypress 可直接读取 AXE API 返回的 violations 数组,但重点在于验证“修复是否真生效”。
使用 Puppeteer + Chrome 将 HTML 渲染为中文 PDF,自动处理图表等待、Tab 展开、动画、测高、白边消除、防分页,适用于看板、报表、网页和交互图表转 PDF。
容易踩的坑:
- 只检查
aria-label存在,却不验证它是否被屏幕阅读器实际读出(例如父元素aria-hidden="true"会屏蔽子元素) - 用
page.getByRole("button", { name: "Submit" })定位,但没确认该按钮是否在tabindex链中可聚焦 - 测试表单提交成功后,没检查错误提示区域是否用
aria-live="polite"声明,且文本是否出现在 DOM 中
推荐做法:
- 对每个表单字段,断言其
computed accessible name等于预期值(Playwright 的accessibility.snapshot()可导出) - 模拟键盘操作:
await input.press("Tab")后,检查document.activeElement是否为下一个逻辑控件 - 用
axe-core的disableRules排除已知不可控项(如第三方 widget),聚焦自己代码修改的部分
人工审计必须绑定具体 DOM 节点
团队内做可访问性走查时,“这个按钮看起来没问题”毫无价值。每次审计必须输出可复现的节点路径和失败条件。
例如:
- 路径:
body > main > form#checkout > div.shipping > fieldset:nth-child(2) > legend - 失败条件:该
legend内容为空,且未用clip-path隐藏(导致屏幕阅读器朗读 “blank”) - 修复验证:插入文本后,用 VoiceOver 的 rotor 检查是否出现在 “landmarks” 列表中
复杂点在于:同一份 HTML,在 Chrome + NVDA、Safari + VoiceOver、Edge + JAWS 下行为可能不同。真正健壮的机制,是把这三组组合的测试结果都纳入准入门槛——而不是只挑最容易通过的那个环境。


















