加入收藏 | 设为首页 | 会员中心 | 我要投稿 站长网 (https://www.027zz.com/)- 区块链、应用程序、大数据、CDN、数据湖!
当前位置: 首页 > 运营中心 > 建站资源 > 建站经验 > 正文

视障友好:后端工程师的无障碍建站避坑指南

发布时间:2026-11-04 14:15:10 所属栏目:建站经验 来源:DaWei
导读:  去年十一,老周在机房三号柜后蹲着换热插拔电源模块,我叼着半截没点的烟——其实没点,就叼着,手在抖,不是怕烫,是空调刚跳闸第三回,UPS嗡嗡响得像有人用指甲刮黑板。那会儿小张在隔间喊“表单提交没触发声音反馈”,我头都

  去年十一,老周在机房三号柜后蹲着换热插拔电源模块,我叼着半截没点的烟——其实没点,就叼着,手在抖,不是怕烫,是空调刚跳闸第三回,UPS嗡嗡响得像有人用指甲刮黑板。那会儿小张在隔间喊“表单提交没触发声音反馈”,我头都没抬:“你让前端改 aria-live。”他停了两秒,说:“后端返回 201 之后,我们清空 input,但 screen reader 没扫到新状态。”我咬住烟嘴又松开,烟掉在防静电垫上,滚了一圈,沾了灰。


文章配图,仅供参考

  我原以为这是个 JS 层级的 focus 管理问题。结果扒日志才发现:我们把所有 form action 全 proxy 到 /api/v2/submit,加了统一埋点中间件,中间件里偷偷改了 Content-Type,把 text/html 偷换成 application/json —— 就为了兼容某个老旧网关的响应体校验逻辑。而小张用的 React 组件,submit 后靠 response.headers.get(‘Content-Type’) 来决定是否重载页面。text/html 走渲染流,application/json 走静默成功。但屏幕阅读器压根不听 JS 说了什么,它只管 document.readyState 和 body 里有没有新增的 role="status"。我们没给。我们连 里都懒得写失败兜底,因为“现在谁还关 JS”。老周那时候正把 UPS 风扇滤网抠下来,甩了两下,灰扑簌簌落在他咖啡杯沿上。


  最绝的是那个搜索补全接口。产品提需说“要支持盲人打字联想”,我们吭哧吭哧上了 debounced 请求 + tokenized autocomplete,response 设计得贼漂亮:{“suggestions”: [{“label”: “上海地铁一号线”, “value”: “sh_1”}], “cursor”: “end”}。然后 QA 用 VoiceOver 测,点了输入框,等了十八秒,念:“搜索框,已编辑,零字符。”——它根本没触发建议列表 DOM 更新。为啥?因为我们的 suggestion 下拉是 absolute 定位、z-index 999、visibility: hidden 切换显隐;而 iOS VoiceOver 默认不读 visibility:hidden 元素,除非你手动加 aria-hidden=false + role=listbox。我们没加。我们只加了 data-test-id。数据测试 ID 在无障碍链路里跟废纸一样厚。小张后来在茶水间烧水,壶盖被蒸汽顶得哒哒响,他一边掀盖子一边说:“其实上次审计报告里写了三次。”我没接话。我盯着自己电脑右下角的系统时间,七点零三分,窗外在下雨,雨点砸在机房北窗上像弹珠。


  还有个坑是 session 过期处理。我们所有接口加了 /auth/check 拦截器,过期就 return json {“code”:401, “msg”:“登录失效”}。前端 catch 之后跳 login 页面。看起来很 clean。但有个细节没人碰:当屏幕阅读器正在朗读某个长表格时,突然弹出 modal,焦点被 trap 在 modal 内,而 modal 本身没设置 aria-modal=“true”,也没有 initial focus;用户听见“登录页”,但光标卡在表格第十七行某列里,键盘 Tab 键疯按,语音报:“表格单元格,未选中,空”。他不知道该往哪儿走。那天下午三点四十七分,测试同事阿琳用 JAWS 复现这问题,手指悬在键盘上停了足足二十秒,最后叹气,摘下耳机搁在桌上,耳机线绕了两圈,缠住她半盒褪黑素药片。


  优胜劣汰——这话不是我说狠的,是真实发生的。上个月,市残联那边推了个新接入的读屏适配白名单,准入标准三条:必须通过 axe-core 扫描基础 WCAG 2.1 A 级项;所有关键交互必须提供 keyboard-only flow(含 tab index 语义链);表单提交成功后,必须由 role=“status” 元素宣告结果。我们三个服务挂了两个。不是因为不会写 aria-live,是因为我们把所有状态更新都塞进 useEffect 的 deps 数组里了,而 deps 里有个 Date.now()。每次渲染时间戳变,useEffect 重跑,但 DOM 并没新增 status 元素——我们只是 rerender 了一个空 div。可 axe 不管你怎么 render,它只看 DOM 树此刻有没有 role=“status”。没有。它就判 fail。老周当时在我背后拍我肩,说:“你上次写的那个熔断降级开关,要不要也考虑加个 aria-disabled?”我笑了一下,笑完发现嘴角发酸。


  前两天改 header 缓存策略,顺手看了眼 nginx-access.log 里 /accessibility/health 的请求,三天零七次。其中五次来自同一 IP——是我们运维同事李工,他老婆是视障教师。他自己不会用读屏软件,但他试过戴眼罩抄我们前端的 tabindex 清单。抄到一半,手机响了,是他女儿打来的,问作业本上那个带波浪线的拼音“chuāng”怎么念。他边讲边随手在 log 文件里搜 “aria-” ,搜出来三百七十二行,七十六处写成了 arai- 。他没改。他关了 vim,喝了口冷掉的枸杞茶。


  所以我说,“视障友好:后端工程师的无障碍建站避坑指南”根本不是指南,是个墓志铭。写在 header 里、写在 OpenAPI spec 里、写在 CI 脚本里的每一条约束,都是前任踩出来又没填平的坑。我们现在能做的?下周一上线前,把你所有 302 跳转路径都跑一遍 keyboard-only flow:Tab 进去,Enter 提交,Tab 到下一步按钮,别碰鼠标。能做到这个,别的先放放。做不到?那你得先修好你的键盘 Alt 键——我上周发现它卡住了,按下去弹不回来,像所有没修好的事一样,悬在那儿。

(编辑:站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!