模板引擎不保证可访问性,需手动注入aria属性、管理焦点并校验语义;mustache等逻辑less引擎需预处理数据以支持aria;ssr阶段必须输出tabindex、autofocus等属性;须通过单元测试、自定义lint和人工at测试保障可访问性。

模板引擎本身不保证可访问性,必须手动注入语义属性
用 Handlebars、Mustache 或 Nunjucks 渲染一个按钮,{{> button}} 输出 <button>{{text}}</button>,但屏幕阅读器根本不知道它关联哪个表单、有没有禁用状态、是否正在加载。模板引擎只管结构拼接,不负责 ARIA 属性、焦点管理或语义校验。
实操建议:
- 所有组件模板必须显式声明
role、aria-labelledby、aria-describedby等属性,不能依赖“默认就对” - 用数据字段驱动 ARIA 状态:比如传入
{ disabled: true, loading: false },模板里写<button disabled aria-disabled="true"></button> - 避免在模板里硬编码
id值;改用动态生成,如id="{{idPrefix}}-submit",并在调用时传入唯一前缀 - 每个组件模板顶部加注释说明其可访问性契约,例如:
<!-- @accessibility: requires `labelId` to be passed, links to <label for="{{labelId}}"> -->
Mustache 不支持条件逻辑,无法处理复杂可访问性分支
Mustache 是纯逻辑less 模板,{{#if}} 不存在——它只认真值/假值,且对 null、undefined、空数组一概视为 false。这意味着你没法在模板里写 {{#if ariaHasPopup}}aria-haspopup="{{ariaHasPopup}}"{{/if}} 这种带值透传的逻辑,只能靠预处理数据把所有 ARIA 属性塞进一个扁平对象里。
常见错误现象:
- 传入
{ "aria-expanded": "true" },但 Mustache 不允许键名含连字符,直接报错或静默忽略 - 想根据
variant === "primary"自动加aria-current="page",Mustache 无法判断字符串相等 - 服务端渲染时,
aria-live="polite"被当成普通属性插入,但没配role="status",AT(辅助技术)完全无视
解决办法:在数据准备阶段统一转换,例如用 JS 预处理函数:
function normalizeA11yProps(data) {
return {
...data,
'aria-expanded': data.expanded ? 'true' : 'false',
'aria-disabled': data.disabled ? 'true' : 'false',
role: data.isTab ? 'tab' : data.isList ? 'list' : undefined
};
}
服务端渲染模板必须同步输出 focusable 元素的 tabindex 和初始焦点
可访问性不是“JS 加载完再补”,而是从 HTML 第一行就要可操作。如果模板引擎在服务端吐出的 HTML 里,弹窗组件没带 tabindex="-1",且没在 <dialog></dialog> 上设 open 属性,那么用户按 Tab 键会直接跳过整个模态框——这不是前端 JS 能修的,是服务端模板漏了。
关键点:
- 所有可聚焦容器(
<dialog></dialog>、<nav></nav>、自定义<accordion></accordion>)必须在服务端输出时就带tabindex="-1" - 若组件需自动获得焦点(如模态框打开),服务端不能只输出结构,还得输出
autofocus属性或配套 JS 初始化标记(如data-autofocus="true") - 使用 Pug/Nunjucks 时,避免用
include直接拼 HTML 片段——它不校验嵌套语义,容易把<header></header>插到<main></main>外面破坏大纲层级 - SSR 模板中禁止用
innerHTML动态注入内容,否则 AT 读不到服务端已渲染的文本
构建时静态分析无法覆盖模板运行时 ARIA 错误
像 axe-core 或 eslint-plugin-jsx-a11y 这类工具,只检查 JSX 或静态 HTML 文件,对 {{> card}} 这种模板引用毫无感知。你改了 card.hbs 里漏掉 aria-label,CI 不会报警,上线后屏幕阅读器用户第一个点击就卡住。
真正能落地的防护手段只有两个:
- 为每个模板写单元测试,用 JSDOM 渲染后断言关键 ARIA 属性存在且值正确,例如:
expect(el.getAttribute('aria-labelledby')).toBe('card-title-123') - 在模板编译流程中加自定义 lint 规则,扫描所有
.hbs文件,强制匹配正则/aria-[a-z-]+=["'].*["']/g,并警告缺失率高于阈值的文件 - 拒绝“视觉验收即通过”的 QA 流程——必须用 VoiceOver + Keyboard 操作走一遍所有模板实例,尤其验证
Esc关闭、Tab循环、Enter/Space触发是否符合 WAI-ARIA Authoring Practices
最易被忽略的是:同一套模板在 SSR 和 CSR 场景下,可访问性表现可能完全不同。服务端输出的 aria-hidden="true" 在客户端 JS 激活后没及时清除,会导致内容对 AT 完全不可见——这种问题只在混合渲染路径下暴露,必须两端都测。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











