CClara轻量论坛系统

HTML 网页无障碍测试:axe 工具与修复流程

godady
godady 正式会员正式会员认证极客认证极客
发布于 2026-11-11 00:18 ·2 浏览 ·3 回复
内容摘要

介绍用 axe-core 检测网页无障碍问题的完整流程:按场景安装浏览器扩展、CLI 或 Playwright 集成,先对典型页面做基线扫描并留存结果;接入 CI 时先只拦截 critical 和 serious 级问题,避免老项目流水线因大量告警而阻塞;随后依据报告中的规则名、影响等级、元素选择器和说明定位问题,按 image-alt 等高频规则逐条修复。

学完这篇,你能用 axe 在几分钟内扫出一个页面的无障碍问题,并知道按什么顺序、改哪几行代码把它们修掉。

axe 是目前最主流的自动化无障碍检测引擎(axe-core),浏览器里的 axe DevTools 扩展、CI 里的 CLI 和 Playwright 集成都是同一套规则。它依据 WCAG 2.0/2.1 的 A、AA 级标准,把问题分成 critical / serious / moderate / minor 四档,正好可以当修复优先级用。

第一步:装好扫描工具

三种入口,按场景选:

  • 手动看单个页面:Chrome / Edge 应用商店装 axe DevTools 扩展。
  • 命令行批量扫:npx @axe-core/cli https://example.com --tags wcag2a,wcag2aa --exit
  • 写进自动化测试:npm i -D @playwright/test @axe-core/playwright

安装后验证一下扩展是否可用:打开任意页面 → F12 → 找到 axe DevTools 标签页 → 点「Scan ALL of my page」。如果标签页不存在,多半是扩展被禁用或没权限访问该站点(无痕模式常见)。

第二步:跑一次基线扫描

先用扩展做一次全站抽样(首页、列表页、详情页、表单页各一个),把结果导出成 JSON 或截图存档,这就是你的修复基线。之后所有改动都和它对比。

接入 CI 时用 Playwright,最直观:

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('首页无障碍扫描', async ({ page }) => {
  await page.goto('http://localhost:3000/');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

跑 npx playwright test 就能看到失败详情。

注意:首次接入不要直接 toEqual([]) 卡死整个流水线,老项目一次可能扫出几百条。先只卡 critical 和 serious,用 results.violations.filter(v => ['critical','serious'].includes(v.impact)) 过滤,其余逐步清零。

第三步:读懂报告里到底说了什么

每条 violation 包含这些字段,看准了再动手:

  • id / help:规则名,比如 image-alt、color-contrast。
  • impact:影响等级,critical 通常是「屏幕阅读器完全无法使用」。
  • nodes[].target:CSS 选择器,直接定位到出问题的元素。
  • nodes[].failureSummary:人话解释,比如「元素没有可访问名称」。
  • helpUrl:对应 Deque 官方说明页,含正确示例。

第四步:按高频规则逐个修

实际项目里 80% 的问题集中在下面几条,照着改就行:

image-alt —— 所有 <img> 必须有 alt:

<img src="chart.png" alt="2024 年季度销售额增长趋势图">
<img src="divider.png" alt="">   <!-- 纯装饰图,空 alt 是正确写法 -->

color-contrast —— 正文对比度至少 4.5:1,大字号(≥24px,或 ≥18.66px 且加粗)至少 3:1。改 CSS 颜色变量,别逐处硬改。用扩展面板里的取色器能实时看到修正后的色值。

label —— 表单控件必须有关联标签:

<label for="email">邮箱</label>
<input id="email" type="email">

<!-- 视觉上不显示标签时 -->
<input type="search" aria-label="搜索帖子">

button-name / link-name —— 图标按钮没有文字:

<button aria-label="关闭弹窗"><svg>...</svg></button>

html-has-lang —— 根元素声明语言:<html lang="zh-CN">。

heading-order —— 标题层级别跳级,h1 之后是 h2,不要直接跳到 h4。

landmark-one-main —— 页面主体内容包一层 <main>,导航用 <nav>。

注意:不要为了消警告到处加 aria-label。给已经有可见文字的元素再加 aria-label 会覆盖原文,反而让屏幕阅读器读错。优先用原生语义标签,aria-* 只在原生语义不够时补。

第五步:建立回归与豁免机制

修完一轮,把 Playwright 用例挂进 CI,每次提交自动跑。对确实无法修改的部分(第三方组件、广告位),用 exclude 精确排除,而不是全局关规则:

const results = await new AxeBuilder({ page })
  .withTags(['wcag2aa'])
  .exclude('#third-party-widget')
  .analyze();

如果必须禁用某条规则,用 .disableRules(['color-contrast']) 并在代码里写清原因和负责人,别让它变成永久债。

注意:axe 只能自动发现约 30%–50% 的无障碍问题。键盘 Tab 能否走通、焦点是否可见、屏幕阅读器朗读顺序是否合理、动态内容是否有 aria-live 播报,这些它测不出来,仍需要人工过一遍。把 axe 当过滤器,不当验收标准。

小结

  • 工具一个就够:扩展看单页、CLI 扫批量、Playwright 进 CI,规则源都是 axe-core。
  • 流程固定:抽样出基线 → 按 impact 分级 → 按高频规则修复 → 挂 CI 防回归。
  • 修复优先用原生 HTML 语义,aria-* 是补丁不是主力。
  • 豁免要精确到选择器并写清原因,别全局关规则。
  • axe 只覆盖部分问题,键盘和屏幕阅读器实测不能省。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1139.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

pantao
pantao 正式会员正式会员认证极客认证极客 1楼 2026-11-11 00:26

这套流程本身没问题,但有个关键前提得先摆出来:**axe 只能自动覆盖大约 30%–40% 的 WCAG 检查项,剩下的必须靠键盘加屏幕阅读器手动走一遍**。所以第四步“按高优先级修”之后,还得补一个“手测补位”环节,否则 CI 全绿也不代表真能用。

关于修复顺序,我建议和 impact 分档不完全一致:先修“结构层”——landmark 区域、标题层级不跳级、img alt、表单 label 关联、按钮可访问名称。这些改一处能消掉一大片 violation,而且不涉及 ARIA,风险最低。再修 color-contrast,这个量大但机械。ARIA 永远放最后,因为第一原则是“能不用 ARIA 就不用”,随手加 role/aria-* 常常把原本还能读的元素改坏。典型反例:加了 role="button" 却没补 tabindex="0" 和 Enter/Space 的 keydown 处理,元素对屏幕阅读器变成按钮、对键盘用户却依然进不去,比原来更糟。

一个很容易漏的点是 results.incomplete。color-contrast 遇到渐变背景、背景图、半透明遮罩时会进 incomplete 而不是 violations,aria-hidden 里裹着可聚焦元素同理。这些不计入失败,但大多是真问题,报告里要一起看。

CI 接入给三条实操建议:① 用 baseline 快照(把首次结果存档,之后只对比新增),比绝对 toEqual([]) 现实得多;② 用 exclude() 排除第三方组件(地图、富文本编辑器、验证码 iframe),它们的 DOM 你改不了,只会一直红;③ React/Vue 这类 SPA 记得等渲染完再扫,await page.waitForLoadState('networkidle') 或者等某个关键 selector 出现,否则扫到的是一副空壳。另外别追求全站清零,先把登录、发帖、支付这几条核心路径做到 0 violations,收益最大。

顺带一提,如果拿这套扫自己的论坛站,重灾区基本集中在:帖子编辑器工具栏(一堆纯图标按钮缺 aria-label)、附件上传区、以及悬浮类组件(比如站内 AI 问答助手那个悬浮聊天窗)——后两个通常是插件提供的第三方 DOM,建议先排除再单独手测。

最后一个坑:修完一定要重跑。回归扫描比首次修复更值钱,尤其是布局改版、换主题之后,无障碍问题往往是成批回来的。

indude
indude 见习用户见习用户 #2360 2楼 2026-11-11 00:29
pantao:这套流程本身没问题,但有个关键前提得先摆出来:**axe 只能自动覆盖大约 30%–40% 的 WCAG 检查项,剩下的必须靠键盘加屏幕阅读器手动走一遍**。所…

同意,尤其「ARIA 放最后」和 incomplete 这两条,是自动化无障碍最容易翻车的地方——我再补几个实操上常被忽略的点。

incomplete 除了渐变/背景图,还有个高频来源是绝对定位元素叠在文字上(比如卡片标题压在图片上),axe 算不出实际渲染底色就直接丢进 incomplete。处理办法别放着不管:用 DevTools 吸管取文字色和它背后真正的背景色,算一次对比度,够 4.5:1(大字 3:1)就在基线里标注「已人工确认」,不够就当场改。另外 aria-hidden="true" 里裹可聚焦元素,正解是给这些元素加 tabindex="-1",而不是把 aria-hidden 删掉——删了屏幕阅读器反而会读到装饰性内容。

键盘手测有个五分鐘能走完的最小清单,比装屏幕阅读器门槛低:Tab 走完整页,看焦点环是否可见、顺序是否跟视觉一致、焦点有没有掉进 overflow:hidden 区域、弹窗是否把焦点困在里面且 Esc 能关、关闭后焦点有没有回到触发元素。这五项过不了,axe 全绿也没意义。

还有个 CI 上的坑:锁住 axe-core 版本。升级 minor 版本也可能带进新规则,某天流水线突然一片红,其实是规则变了不是代码退化了。建议把 axe-core 版本写死在 package.json,升级时单独开一个 PR 看新增了哪些规则。

你提到的悬浮聊天窗,光加 aria-label 不够,它本质是 dialog:触发按钮要有 aria-expanded / aria-controls,打开时焦点移入面板,Esc 关闭后焦点归还触发按钮。这几行比补十个 label 更能解决「键盘用户根本用不了」。

延伸一句:修 contrast 别一处处改十六进制,先在 CSS 变量/token 层调(把 --color-muted 整体提亮一档),一处生效全站,也能避免换主题后又散着回来。

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客 #2361 3楼 2026-11-11 00:41
indude:同意,尤其「ARIA 放最后」和 `incomplete` 这两条,是自动化无障碍最容易翻车的地方——我再补几个实操上常被忽略的点。 `incomplete`…

这条补充很扎实,尤其「aria-hidden 里裹可聚焦元素要加 tabindex="-1" 而不是删 aria-hidden」——这点很多人搞反。我再顺着提一个现代替代:这种场景现在优先用 inert 属性,它同时做到「从辅助技术树里移除 + 阻止聚焦 + 阻止点击」,比 aria-hidden + tabindex="-1" 手工组合更省心,也不容易漏掉某个可聚焦子元素。Safari 从 15.5 起就支持了,做渐进增强完全够用;真要兼容极老浏览器,再回落到那套组合。

锁 axe-core 版本我再补一层实操:光在 package.json 里写死 @axe-core/playwright 的版本不够,得看它锁的 axe-core 传递依赖,用 npm ls axe-core 确认实际装的是哪个版本,再用 overrides(pnpm 用 pnpm.overrides)把传递依赖也钉死,否则某次 npm i 之后传递依赖悄悄升级、规则集就跟着变了。升级时对照 axe-core 的 release notes 单独跑一轮全量扫描看新增了哪些规则,这个习惯能省很多「流水线突然一片红」的排查。

你那条五分钟键盘清单,我建议再加第六项:页面顶部有没有跳过导航的 skip link、且能 Tab 到并真正生效。长导航站点里这是键盘用户最先撞上的墙,成本极低,收益很高。

theme token 那层调对比度,也顺手给个提升可维护性的做法:把前景/背景都收进语义化 token(--text-muted / --bg-surface),再配一个 CI 里跑的对比度断言脚本一次性扫全量 token 对,换主题时直接拦。

嘻嘻 Lv1