Web可访问性测试实战:从自动化扫描到键盘与读屏走查的完整指南
做Web开发这些年有一个场景我猜大家都经历过项目排期压得死死的需求评审时没人提可访问性等上线前夜测试突然问了一句“这个下拉菜单读屏软件能读出来吗”会议室里瞬间安静。那个瞬间我意识到Web可访问性A11y在大多数团队里不是没有被重视而是根本不知道从哪开始测、测什么、怎么测。这篇文章我想用自己的实操经验把A11y测试这件事从头到尾说清楚——它适合任何写前端、做产品、管质量的同学参考不需要你先去完整啃一遍WCAG文档只要你手上有一个正在开发的Web页面就能照着这套方法跑起来。先说一个我的核心观点A11y测试不是“找茬”本质上是把交互设计的边界条件补完。无障碍做得不好的产品通常不是“功能缺失”而是“路径断裂”——一个按钮没有可访问名称读屏用户整个流程直接卡死一个弹窗焦点没锁住键盘用户永远跳不出模态框。这种问题用自动化工具扫不出来靠人工手册又容易漏。所以真正可靠的方案是分层次结合自动化扫描打底、键盘走查跟进、读屏实测兜底最后再把规则固化到CI里让它变成每次提交的强制检查而不是上线前的临时突击。1. 先搞懂A11y测试到底在测什么为什么它总被排到最后一刻1.1 A11y问题的本质是“路径断裂”不是“界面丑”很多团队对A11y的第一印象是“给页面加alt属性”“把颜色对比度调高一点”这其实把问题看小了。我在项目里见过最典型的案例是一个筛选器组件视觉上是一个带搜索的复选框列表鼠标操作完全正常但它没有绑定任何ARIA角色也没有键盘事件读屏用户Tab到这个组件时听到的只有“组合框”进去之后方向键完全无响应整个筛选流程就废了。这种问题就是典型的“路径断裂”——功能在常规交互下是好的但在辅助技术下根本走不通。所以在规划测试时我习惯先画出产品里最高频的用户路径比如“搜索-筛选-查看结果-详情页-下单”这样一条完整链路然后针对这条链路的每一段做可访问性检查。比单纯按页面扫描更有效因为A11y问题的严重程度发生在“连续操作”中单一页面的孤立检查容易漏掉焦点流转、状态播报这些跨步骤的问题。1.2 排期上A11y总是最后一位但修复成本却是“越晚越贵”我自己的经验是A11y测试最适合放在开发完成、功能测试刚跑完的时间点而不是等UAT甚至上线前。原因很简单越到后期修复A11y问题越容易牵一发而动全身。比如你发现购物车数量按钮的点击区域只有20像素高键盘用户根本没法精准操作这时候改涉及样式、布局、可能的交互重构而如果开发阶段就加入检查可能只是加两行CSS和几个ARIA属性的事。另外还有一点经常被忽略A11y问题不只是对残障用户有影响。临时用键盘操作的人、老年人、弱视用户、今天光线很强的户外场景都会因为可访问性做得不好而流失。所以从这个角度看A11y测试本质上是在扩大产品的可用人群边界这比任何“加分项”式的需求都实在。2. A11y测试的四个层次自动化扫描只是起点不是全部我把实际工作中的A11y测试拆成四个层次每一层都有对应的工具和方法层层递进缺了哪一步都等于没测完。这个划分方式是多年实践中沉淀下来的现在给团队做内部分享也一直在用。2.1 自动化扫描效率最高但只能查出“语法错误”第一层是自动化工具扫描。你只需要把页面打开让工具跑一遍就能得到一份问题清单。这类工具里我常用的是axe-core也可以直接通过Lighthouse里集成的A11y板块一键扫描。自动化扫描擅长什么它擅长检查那些“有明确规则、可枚举”的问题比如图片有没有alt属性alt是否为空按钮和链接是否有可访问名称accessible name表单label是否正确关联到表单控件色彩对比度是否达标ARIA属性是否拼写错误是否用了不被支持的ARIA角色标题h1-h6层级是否跳级但它的边界也很明显它不知道你的交互逻辑是什么也判断不了焦点顺序是否符合用户预期。我遇到过最典型的一个“自动化扫描全绿、实际体验全红”的案例一个自定义下拉菜单代码里ARIA属性写得规范齐全扫描零问题但真正用键盘操作时打开后面板的选项无法用上下键切换。这种“属性对但行为错”的问题自动化工具完全无能为力。所以自动化扫描的正确用法是把它当作“第一道过滤网”而不是“最终裁判”。2.2 键盘走查可能比读屏更快发现问题第二层是键盘走查。这个方法门槛极低不需要任何工具把鼠标拔了或者不碰鼠标纯用键盘把核心用户路径完整走一遍。我会重点检查以下几点Tab是否按视觉顺序流动顺序是不是符合操作预期被Tab聚焦的元素有没有清晰的焦点指示focus visible弹窗、抽屉、自定义下拉面打开后焦点是否被正确移入关闭时是否正确返回触发控件回车键和空格键能否触发按钮操作方向键在单选组、Tab列表、滑块等自定义控件里是否生效键盘走查是性价比最高的手动测试。我经常在设备上连一个实体键盘做完整操作十分钟能走完一条核心链路但能发现交互层的绝大多数低级问题。可以做一张简单的记录表每走一个页面记录三个结果能否到达这个控件、能否操作这个控件、能否顺利完成这个操作。2.3 读屏实测最接近真实用户也最容易“吓到”团队第三层是用屏幕阅读器做真实测试。Windows上主流是NVDA配合Chrome浏览器macOS上就是VoiceOver配合Safari或Chrome。读屏测试最直接的价值是验证“组件语义是否正确表达”按钮的提示文字读出来是否完整比如“关闭弹窗”就该只读这个不需要读出一堆无关内容表单错误提示能不能被读屏及时播报而不是安静地显示在视觉区域动态更新的内容比如“加载完成”“发送成功”有没有被主动提示自定义控件的角色、状态、属性有没有被正确传递。读屏实测的启动门槛不高但对新手来说第一次听NVDA读网页会非常不适应因为它是逐行快速朗读的语速、视点都和视觉浏览完全不同。我个人的习惯是先用NVDA把“首页标题-导航-搜索-内容区-页脚”从头到尾读一遍听有没有明显的“无名称控件”“未标记区域”之类的问题再进入核心交互流程做定向检查。这个阶段通常能筛出自动化扫描完全看不到的体验问题。2.4 第四层回归与扩展验证不能漏的“内容层面”第三点之外还有一个不太起眼但极其重要的层次是“内容可读性与扩展验证”。这层常常被人遗忘但我始终保留着。它说的是即使页面在交互上无障碍内容本身是不是容易被所有人理解、识别、消化。一个链接的文字“点击这里”对读屏用户来说就是无意义噪音一个用省略号截断的商品名视觉上很干净但读出来信息是不全的用户可能根本不知道那是什么商品。把这个层次放进A11y测试里是想提醒团队无障碍并不仅仅是辅助技术能读而是任何人都能凭借最少的信息完成同样的任务。自动化工具在这里同样无能为力唯一有效的方法是用真实场景和真实文案逐条核对。3. 搭建一套“能守住门”的自动化A11y测试体系跑通单次扫描不难难的是让A11y检查成为开发流程里自动执行的一道闸门。这一年我花了很多精力把A11y测试固化到代码提交的前置检查和CI流程中团队的执行力因此明显提升。3.1 工具选型与环境准备自动化方面我当前的主力组合是这样几个工具配合使用axe-core核心规则引擎几乎所有主流测试框架都有对应的封装库比如axe-core/playwright、jest-axe。eslint-plugin-jsx-a11y在写代码阶段就能拦截一部分A11y问题直接在编辑器里显式报错这是成本最低的一层。Playwright axe-core/playwright面向关键页面做端到端的A11y断言能配合真实路由、登录态、动态渲染做扫描。Pa11y CI用来批量扫描一组页面并产出报告适合做定时或发布前回归。这套组合的价值在于分层ESLint管“代码写错”axe管“渲染后结构错”端到端扫描管“动态内容错”。每个阶段都有反馈问题越早发现修复成本越低。提醒一个容易被忽略的点不管用哪种工具需要跑A11y扫描的页面一定要是真实的业务页面状态最好带上用户登录态和真实数据因为很多A11y问题是在动态渲染内容后才出现的。用静态壳页面扫描基本就是在给空气做质检。3.2 接入示例给一个Vue组件加A11y断言以一个Vue项目为例最简单的做法是在组件测试里用jest-axe检查渲染结果。下面是一个最小可用的示例import { mount } from vue/test-utils import { axe, toHaveNoViolations } from jest-axe import FilterPanel from /components/FilterPanel.vue expect.extend(toHaveNoViolations) test(筛选面板不应产生A11y违规项, async () { const wrapper mount(FilterPanel, { props: { visible: true, }, }) const results await axe(wrapper.element) expect(results).toHaveNoViolations() })这段代码的关键在于它会自动按照axe-core内置的规则对渲染之后的DOM做完整检查如果有任何规则不满足测试就会失败。让这条断言生效的前提是你的组件在挂载阶段把所有渲染都完成了。如果一个组件里还存在异步加载的节点测试就得用flushPromises或waitFor确保内容稳定后再扫描否则会因为“还没渲染完”而漏报。3.3 CI上的差异化配置按严重级别和页面类型设置阈值把所有A11y扫描都设成“零容忍”是理想状态但对存量项目不现实因为存量代码里的历史问题可能一次性爆出几十上百条。我建议按严重级别拆梯度CI上把严重违规比如表单无label、按钮无可访问名称设为阻断项只warning不阻断的留给后续迭代。我一般是这么配的以Lighthouse CI为例{ ci: { collect: { url: [https://example.com/product-list, https://example.com/product-detail, https://example.com/checkout], numberOfRuns: 3, settings: { preset: desktop, onlyCategories: [accessibility] } }, assert: { assertions: { categories:accessibility: [error, {minScore: 0.9}] } }, upload: { target: temporary-public-storage } } }这里我把A11y分数阈值设为0.9低于90分则发布阻断。实际项目中可以根据团队节奏逐步上调成熟之后再去冲击100分。另一个建议是把关键页面列表维护成一份配置比如购物流程页面、登录注册页、个人中心页而不是全站所有页面无差别扫描这样既节省CI时间又能把火力集中在影响核心转化率的地方。等到自动化完全稳定再把范围逐步扩大。4. 手动A11y走查清单键盘、焦点、对比度、表单逐项怎么查自动化解决的是规则性问题但真实的无障碍体验还得靠人手工走查。下面这份清单是我每次项目自查时使用的手动走查手册按模块划分建议打印出来或者放在文档里走查时逐项打勾。4.1 键盘可达性Tab顺序、焦点可见、快捷键清单一第一块是键盘操作。打开一个页面后从左上角开始按Tab从头到尾走一遍不要用鼠标辅助。焦点流动顺序是否和视觉排版顺序一致用户看到的第一眼是不是就是焦点进入的地方每个Tab停留的元素有没有清晰可见的焦点环很多团队会自定义outline结果把它干掉了这是极大的坑。进入弹窗或抽屉后焦点是否自动移入并且被锁定在里面不能Tab到背后页面关闭时焦点是否回到触发按钮所有可交互的元素按钮、链接、输入框、下拉框是否都能被键盘操作不只是Tab到还能用回车、空格、方向键完成操作实际走查中最常翻车的是自定义组件的键盘行为。我见过一个日历控件日期单元格用的是div加click事件没有tabindex也没有键盘事件鼠标用户随便点键盘用户什么都做不了。这种问题的根源是开发用了“看起来像控件但不是原生控件”的标签来制作交互组件手动走查最容易暴露这类问题。4.2 焦点管理与焦点可见性不只是outline的事第二块是焦点。很多人以为焦点管理就是“别把outline去掉”其实不完整。焦点管理要查三件事焦点去哪儿、焦点能不能被看见、焦点不会“飘到看不见的地方”。先说焦点去哪儿。弹窗打开时焦点应该移进弹窗而不是留在背后的触发按钮。如果弹窗提供关闭按钮关闭后焦点应该回到原触发元素否则用户按Tab时场景错乱。其次动态插入的内容比如“提交中”的loading遮罩不应把焦点强行夺走也不应让用户焦点指向被隐藏的元素。第三焦点可见性中最大坑不是outline而是“自定义颜色太浅”比如用了淡灰色、淡蓝色描边在浅色背景上根本看不见。建议在开发规范里直接约定focus样式用高对比度边框或双描边避免后期反复测。4.3 颜色与对比度重点从正文文本扩展到图标、边框和占位符第三块是颜色。WCAG里最常被引用的标准是正文文本对比度必须达到4.5:1大号文本和UI组件边界达到3:1。但手动检查时最常被忘记的恰恰是那些“不算正式文本”的元素占位符placeholder文字如果淡到只剩灰色读是能读出来如果读屏支持但视力一般的人看不到严格说也算信息层级缺失。必填星号、警示图标、错误提示它们本身不是单纯文本而是“与文本绑定的语义信号”对比度不足会影响识别。图表和图例饼图里相邻两个扇区颜色对比不足用户根本分不清哪块是哪块。边框输入框的边框对比度不足用户无法辨认可交互区域的边界。手动检查时不需要挨个像素量用工具配合就行。我通常用axe的自动规则先扫一遍基础对比度问题再用Lighthouse手工补充检查图表、边框这类非文本内容。更准确的做法是用Figma或浏览器DevTools里的对比度计算器直接计算。总之对比度检查的重点不只是正文你需要把信息层级中所有“有功能”的颜色都考虑进去。4.4 表单label与错误提示为什么自动化扫过还是有问题第四块是表单。自动化能查出“这个输入框没有label”但查不出“这个label在视觉上倒是对的读屏顺序却是错的”。手动验证时我侧重以下几点每个输入框、下拉框、单选/复选框都有关联的label。关联方式推荐用label元素配合for和id而不是用一个span加aria-label后者在部分读屏下没问题但一旦label变更维护成本高。错误提示必须与对应输入控件建立关联常见做法是把错误信息用aria-describedby关联到input并设置aria-invalidtrue。如果没有关联读屏用户根本不知道错在哪里只知道“提交失败”。必填标记要能被读出来。不能只靠红色星号需要包裹在“必填”文本中或使用aria-required。多步骤表单里每一步的说明标题fieldset/legend能否被读出来特别是单选组和复选框组没有legend的fieldset等于失去了组语义。表单测试最容易出问题的场景是“视觉正确但语义缺失”某字段没有label但视觉上放了一个说“请输入邮箱”的占位符。输入时没问题但读屏用户根本不听占位符他们需要的是字段名——这一项自动化测不出来需要人工验证。5. 那些年我们一起踩过的A11y坑以及对应的修复方案光讲理论不够我把团队里真实处理过的几个典型问题写下来附上完整修复方向。这些案例几乎都是自动化扫描扫不出来、必须手动或端到端结合才能定位的希望能给同样遇到这些问题的团队节省一些排查时间。5.1 自定义下拉框“没有身份”键盘能走读屏听不到场景产品列表页有一个筛选项前端用div模拟下拉选项选项用li渲染。自动化扫描零问题键盘也能操作但读屏用户听到的只有“组合框”没有任何选项名称。定位思路先检查这个自定义组件的ARIA属性。问题通常出在「没有用rolelistbox给可选项列表定义语义」上且选项项本身缺少roleoption。修复的方向是给下拉框补上完整ARIA设计模式容器元素加rolelistbox每个选项加roleoption和aria-selected面板打开时通过aria-expanded通知状态再用aria-activedescendant关联当前激活选项。这里最需要注意的坑是“属性齐了但事件没跟上”ARIA属性本质上是给读屏的一个接口协议你要保证aria-selected的值真的会随着键盘上下选择实时更新而不是固定在某个静态值。我处理过的一个案例就是属性全对但选中的视觉界面和读屏状态完全不同步因为JavaScript没有在keydown事件里去改写aria-selected。修复这种问题的思路简单说就是先码语义再码状态最后码事件。5.2 模态框焦点“锁不住”背景页面还能Tab到场景一个编辑资料弹窗打开后按Tab键焦点会跑到弹窗背后的页面元素上读屏用户更是可以读到整个弹窗背后的字段整个页面变成“可穿透”的状态。定位思路模态框的焦点管理不是“弹窗打开后把焦点移进去”就结束了你必须锁定焦点在弹窗内部循环并且关闭时把焦点还给原来的触发按钮。最稳妥的方案是用原生dialog元素以及showModal()它自带焦点锁定、模态语义和Esc关闭。如果是更复杂的自定义弹窗则必须手动维护一个焦点循环监听Tab键当焦点移动到弹窗容器的最后或第一个可聚焦元素时强制折返。我在项目里通常还会加一个“背景惰性化”的处理——把弹窗背后的主要内容区域用inert或其他等效方式整体设为不可交互这是一个很有效的辅助手段能防止键盘用户意外走到背景里。手动实现焦点循环要特别注意列表顺序因为弹窗内部可能有多个可聚焦组件输入框、关闭按钮、正文链接它们必须按照合理的顺序排列而不是随意跳跃。5.3 动态内容“静悄悄”读屏用户根本没听到操作结果场景页面提交表单后顶部显示“提交成功”提示视觉上很清楚但读屏用户完全没感知。功能点上没有错误但从无障碍视角看这是一条断掉的路径。定位思路动态更新的内容如果不依赖用户主动焦点是不会被读屏注意到的——因为读屏并不会“实时监听”整个网页所有区域的变化。解决方案是给需要播报的容器增加aria-live属性。aria-live有两个值polite等用户空闲时播报适合“保存成功”这类提示和assertive立即播报打断当前操作适合紧急错误。实际使用中我倾向于把aria-live放在“结果提示区域”的容器上并且确保这个容器在页面初始加载时就存在于DOM中不要等到提交成功时才动态创建否则部分读屏仍然不会播报。这里还有个细节如果提示文本是反复变化但内容相同读屏不会重复播报。开发阶段最好在错误文本中拼上错误码或者变化的状态值让读屏感知到这是新信息。5.4 图片alt要么没有、要么全写成文件名场景一个内容丰富的商品详情页跑了axe扫描alt标签问题一大堆。点开一看一半图片alt写的是空字符串另一半写的是图片文件名比如“xxx_product_0815.jpg”。从规则上空alt和文件名的alt都能通过某些检查但对用户毫无意义。定位思路alt的处理规则其实很清晰。装饰性图片纯视觉分隔、背景可以用空alt并标记为presentation让读屏跳过信息性图片商品图、流程图、图标必须给出能描述内容的文本。我认为最实用的判断标准是假设别人看不到这张图只靠读屏的描述能不能准确理解你原本想传达的信息。能就算合格不能就重写。另一个容易忽略的是功能图标的alt。比如购物车旁边的垃圾桶图标如果alt写的是“垃圾桶”而不是“删除该商品”读屏用户就会听到一串意义不明的名词。图标按钮的可访问名称以“动词对象”的形式写更合理这可能是所有A11y修复里成本最低、收益最直接的一条。6. 一些实用的收尾建议让A11y测试从“一次性任务”变成“日常习惯”写完这么多我还是想强调一件事A11y测试如果在项目里只是“某次发布前突击做一次自动化扫描”那它很快就会形同虚设。真正有效的做法是把它拆成几个循环嵌入到不同角色和不同阶段里。开发阶段让ESLint插件在编码时就拦住一部分可访问性错误这是成本最低的拦截点。提交代码后让CI在你每次push时跑关键页面的A11y扫描如果不达标就阻断合并请求。迭代阶段至少保证每个Sprint里的新增组件有一个jest-axe的A11y用例保证核心路径每周手动走查一次尤其是键盘和读屏实测。给这家团队做分享时我常建议他们各写一份“迷你文档”开发的A11y检查清单、测试的A11y走查清单、产品的A11y发布验收单。这三份文档不需要很复杂每份一页纸就够关键是里面的项目是你自己团队的产品真实涉足过的路径和易错点而不是抄一份通用WCAG要点。通用规范是死的你的产品路径是活的一份属于自己团队的清单比什么标准文档都有用。如果现在你的项目还没有做过任何A11y测试我建议从一条事开始把核心购物流程用键盘完整走一遍然后打开NVDA把每个页面从头到尾读一遍。做完这两件事你大概率会发现一堆之前完全想不到的问题而且修复起来并不复杂。先跑通这个流程再回头用自动化规则做固化这个顺序是我自己在多个项目上反复验证过的稳。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →