组件无障碍访问
无障碍软件和辅助技术使残障用户也能使用你构建的产品。开发组件时应确保所有用户都能感知、理解、导航和交互。基础组件提供内置的无障碍支持;自定义组件需自行实现。推荐遵循 WCAG 指南。
无障碍属性 —— Labels 与 aria-label
基础组件自动提供无障碍支持——lightning-input 的 label 属性自动通过 for/id 关联到输入字段。当无法使用可见 label 元素时,用 aria-label 为屏幕阅读器用户标识控件:
<lightning-button aria-label="Log In" label="Log In"></lightning-button>
<!-- 生成: -->
重要:aria-label 不可见——仅传递给屏幕阅读器。如果信息对所有用户重要,应让文本可见。上面的例子中 aria-label 实际上是多余的——可见的 label="Log In" 已经足够。只有在没有可见标签或需要为屏幕阅读器提供额外描述信息时才用 aria-label。
将无障碍属性暴露为公共属性
关键:用 @api 控制属性后,属性默认不出现在 HTML 输出中——必须用 setAttribute 反射到 DOM:
privateAriaLabel;
@api get ariaLabel() { return this.privateAriaLabel; }
set ariaLabel(value) {
this.privateAriaLabel = value.toUpperCase(); // 转换值
this.setAttribute("aria-label", this.privateAriaLabel); // 反射到 DOM!
}
// 生成:
// 不加 setAttribute → aria-label 不出现在 DOM 中,屏幕阅读器看不见!
ARIA 属性 —— 高级无障碍
ARIA 属性为屏幕阅读器提供详细信息——如朗读按钮的当前状态。命名约定:HTML 用 kebab-case(aria-pressed),JavaScript 用 camelCase(ariaPressed)。
模板中 id 值会被转换为全局唯一值——不要用 id 选择器在 CSS 或 JS 中,用 class 或 data-* 代替。
<!-- 父组件设置 aria-pressed="true" -->
<lightning-button label="Liked" aria-pressed="true"></lightning-button>
// 组件 JS 暴露为 @api 属性
@api get ariaPressed() { return this.pressed; }
set ariaPressed(newValue) { this.pressed = newValue; }
// 模板中:
默认 ARIA 值与静态值
默认值:在 connectedCallback() 中定义(不是 constructor),消费者提供的值覆盖默认值。消费者未提供时使用默认值。
静态值(阻止修改):空 setter 忽略消费者输入,getter 始终返回固定值——用于不应被改变的核心属性(如 role="button"):
set role(value) {} // 空 setter——忽略消费者
@api get role() { return "button"; } // 始终返回 "button"
跨模板链接 ID 和 ARIA 属性
同一模板内的 ID 和 ARIA 属性自动关联。不同模板间(不同组件)——原生 Shadow DOM 无法跨组件关联 ID。解决方案:用 Light DOM 将两个元素放在同一 shadow root 下:
<!-- container.html (shadow DOM) -->
<c-label></c-label> <!-- light DOM: <label id="my-label"> -->
<c-input></c-input> <!-- light DOM: <input aria-labelledby="my-label"> -->
替代方案:将两元素放同一组件,或通过 @api 属性传递 ARIA 字符串替代 ID 引用。
处理焦点 —— Tab 导航与 tabindex
浏览器完全可通过键盘 Tab 键导航。交互元素(a, button, input, textarea)自动获得焦点。非交互元素(div, span)需 tabindex="0"。
仅支持 0 和 -1:0 = 参与标准导航(按 DOM 顺序);-1 = 移出顺序导航但可编程聚焦(.focus())。
自定义组件的默认行为:焦点跳过组件容器,直接移到组件内部的可聚焦元素——<c-child> 本身不在 tab 顺序中。
焦点 —— 应用到子组件与焦点委托
将组件本身加入 tab 序列:父组件在子组件上设 tabindex="0"——<c-child tabindex="0">。
delegatesFocus(推荐):自定义按钮等组件用 static delegatesFocus = true 自动管理焦点:① .focus() 自动委托到内部第一个可聚焦元素;② 点击 shadow DOM 内不可聚焦区域→第一个可聚焦元素获得焦点(类似点击 label 聚焦 input);③ :focus CSS 同时应用于宿主元素和聚焦元素。
不要同时使用 tabindex 和 delegatesFocus——会打乱焦点顺序。
生命周期钩子 —— 概述
生命周期钩子是在组件实例特定阶段触发的回调方法。LWC 框架完全管理组件生命周期——创建组件、插入 DOM、渲染、移除,并监控属性变化触发重渲染。
五个主要钩子 + 一个方法:
- constructor() [父→子] —— 组件实例创建时触发,此时元素尚未在 DOM 中
- connectedCallback() [父→子] —— 组件插入 DOM时触发,可访问 this.template
- renderedCallback() [子→父] —— 组件完成渲染后触发,注意方向反转!
- disconnectedCallback() [父→子] —— 组件从 DOM 移除或隐藏时触发
- errorCallback() —— 后代组件抛出未处理错误时触发
- render() —— 不是生命周期钩子,是 LightningElement 上的受保护方法,用于条件返回模板
理解流向方向至关重要:constructor 和 connected/disconnected 是父先于子;renderedCallback 是子先于父(子组件完全渲染后父组件的 renderedCallback 才触发)。这是因为父组件包含子组件——父组件"渲染完成"意味着所有子组件也已渲染完成。Wire Service 有自己的数据生命周期,不同于组件渲染生命周期。
constructor() —— 规则与限制
constructor 在组件实例创建时触发,是第一个执行的生命周期钩子。流方向:父→子(父组件先构造,子组件后构造——因为子组件还没被创建)。
此时不可用的内容:不能访问子元素(还不存在);不能访问通过模板渲染的元素(this.template 尚不可用);不能检查元素的属性(attributes)和子元素(children);公共属性也尚未赋值——它们在 constructor 之后、connectedCallback 之前才被设置。
HTML Custom Elements 规范五条强制规则
- 首条语句必须是无参 super():建立正确的原型链和 this 值。始终在访问 this 之前调用 super()
- 不使用 return 语句:除非是简单的提前返回(
return或return this) - 不使用 document.write() 或 document.open()
- 不检查元素的属性和子元素:此时它们还不存在
- 不检查公共属性:它们在组件创建后才设置
常见错误 —— 在构建期间添加宿主元素属性
// ❌ 绝对不要这样做
constructor() {
super();
this.classList.add("new-class"); // 元素尚未在 DOM 中!
}
// ✅ 正确做法 —— 在 connectedCallback 中
connectedCallback() {
this.classList.add("new-class"); // 元素已在 DOM 中
}
你可以在除 constructor 外的任何生命周期阶段向宿主元素添加属性。constructor 的最佳用途仅限于:初始化简单的类字段默认值、设置非响应式状态。所有其他操作——尤其是任何涉及 DOM 或属性的——都应该延迟到 connectedCallback() 中执行。
connectedCallback() 与 disconnectedCallback()
这两个钩子遵循 Web Components 标准。流方向都是父→子。使用 this 访问宿主元素,this.template 访问组件模板中的元素。你还可使用 this.isConnected 检查组件是否连接到 DOM。
connectedCallback()
组件插入 DOM时触发。可用于:
- 与当前文档或容器建立通信,协调行为
- 执行初始化任务——获取数据、设置缓存、监听事件
- 订阅/取消订阅消息频道(Message Channel)
- 使用
lightning/navigation模块导航 - 使用第三方 Web Components
三个关键警告:
- 可能触发多次!例如从 DOM 中移除后重新插入(如列表重排序)。每次插入都会触发。如果代码应该只运行一次,使用守卫标志防止重复执行。
- 无法访问子元素!此时子组件的 connectedCallback 尚未执行——
this.template.querySelector("div")返回 null。不能依赖子组件的 DOM 结构。 - 属性赋值时机:属性在 constructor 之后、connectedCallback 之前赋值。如果组件从属性派生内部状态,将逻辑写在 setter 中比在 connectedCallback 中更好——setter 在属性赋值时即执行,时机更准确。
disconnectedCallback()
组件从 DOM 移除或隐藏时触发。用于清理 connectedCallback 中的工作——清除缓存、移除事件监听器、取消消息频道订阅。
反模式 —— 标记生命周期钩子为 async
绝不要将 connectedCallback() 或 disconnectedCallback() 标记为 async。框架不等待返回的 Promise。如果在钩子中 await 某个操作,await 之后的代码在框架已经继续运行之后才执行——导致渲染、父子回调、事件处理器的执行顺序不可预测。
正确方式:保持钩子同步,调用单独的 async helper 方法:
connectedCallback() {
this.loadData(); // 同步钩子 → 调用 async helper
}
async loadData() {
const result = await fetchSomething();
// 在这里更新响应式属性——安全,框架会处理后续重渲染
}
renderedCallback() —— 渲染后逻辑
renderedCallback 是 LWC 独有的钩子——在组件完成渲染阶段后执行逻辑。流方向子→父(子组件先完成渲染,父组件后完成——因为父组件的渲染包含子组件的渲染结果)。
渲染与重渲染机制
组件连接并渲染后,任何状态变更触发重渲染流程:
- 组件被标记为 "dirty"
- 一个执行重渲染的 microtask(微任务)被入队
当属性值变化且该属性直接在模板中使用或在模板使用的 getter 中被引用时,组件重渲染。模板中所有表达式被重新计算。
DOM 重用与 Diffing 算法
LWC 引擎尝试重用现有元素以最小化 DOM 操作。在以下情况下使用 diffing 算法决定是否丢弃元素:
- for:each 元素:按
key属性决定——如果 key 改变,元素可能重渲染;如果 key 不变,引擎假定迭代元素未变,不重渲染 - Slot 内容:引擎尝试重用 slot 中的元素,但 diffing 算法最终决定是否驱逐并重建
hasRendered 守卫模式
组件在应用生命周期中通常渲染多次。如果需要在 renderedCallback 中执行一次性操作:
hasRendered = false;
renderedCallback() {
if (this.hasRendered) return;
this.hasRendered = true;
// 一次性初始化...(如加载 D3 图表库、初始化第三方组件)
}
事件监听器
首选在 HTML 模板中声明式添加事件监听器。如果必须编程式添加,在 renderedCallback 中进行。不需要移除监听器——如果同一监听器被重复添加到同一元素,浏览器忽略重复。
⚠️ 避免无限循环
在 renderedCallback 中更新组件状态可能导致无限循环——每次变更触发新渲染,渲染完成又触发 renderedCallback:
- ❌ 不要更新 wire adapter 配置对象属性
- ❌ 不要更新公共属性(@api)或模板中使用的字段
- 每次状态变更 → 新渲染 → 新 renderedCallback → 再次变更 → 无限循环
参考:lwc-recipes libsChartjs 组件展示了正确的 renderedCallback 用法。
条件渲染模板 —— render() 方法
render() 不是生命周期钩子。它是 LightningElement 类上的受保护方法——必须存在于原型链上。区别在于:钩子告诉你某事发生了,而 render() 是框架调用来获取模板的方法。可以在 connectedCallback 之前或之后调用。
主要用途:条件性渲染完全不同的模板。创建多个 HTML 文件 → 导入它们 → 在 render() 中根据组件状态返回正确的模板:
import templateOne from "./templateOne.html";
import templateTwo from "./templateTwo.html";
render() { return this.showTemplateOne ? templateOne : templateTwo; }
// 组件结构:
MyComponent/
├── myComponent.js / .js-meta.xml
├── templateOne.html / templateOne.css ← CSS 文件名必须匹配模板名
└── templateTwo.html / templateTwo.css ← 不能引用主组件或其他模板的 CSS
CSS 规则:每个额外模板只能使用与其文件名完全匹配的 CSS 文件。默认模板(与组件同名的 .html)不定义 render() 时框架自动返回它。
推荐:大多数场景下用 lwc:if/elseif/else 在单模板内处理。仅当两个状态的标记结构完全不同时才用多模板——这类似于其他框架中的代码分割模式。
errorCallback() —— 错误边界模式
errorCallback 是 LWC 独有的钩子。实现它来创建错误边界组件——像 JavaScript 的 catch{} 块一样,捕获所有后代组件树中的错误。捕获的错误来源包括:生命周期钩子中抛出的错误、HTML 模板中声明的事件处理器中抛出的错误。
两个参数:error——JavaScript 原生 Error 对象;stack——字符串。
模式一:条件渲染错误/正常视图(推荐)
<!-- boundary.html -->
<template lwc:if={error}>
<error-view error={error} info={stack}></error-view>
</template>
<template lwc:else>
<healthy-view></healthy-view>
</template>
// boundary.js
errorCallback(error, stack) { this.error = error; this.stack = stack; }
当后代组件抛出错误 → errorCallback 触发 → healthy-view 被卸载并从 DOM 移除 → error-view 显示。当错误恢复后,可重新渲染 healthy-view。
模式二:单模板(不使用 lwc:if)
也可以只用单个模板。当组件抛出错误时,errorCallback 触发,框架在重渲染期间卸载组件——将其从 DOM 中移除。不需要显式的条件渲染。
边界放置策略
可以在任意位置定义错误边界:包裹整个应用(全局错误处理)、包裹每个独立组件、或介于两者之间。思考"你希望在哪个层级告诉用户出错了"——这是一个 UX 决策。
关键限制
编程式事件处理器的错误不被捕获!
- ✅ 声明式处理器:模板中的
onclick={handler}→ 被 capture - ❌ 编程式处理器:JS 中的
addEventListener()→ 不被捕获
这是 LWC 框架的已知限制——错误边界只能捕获框架管理的声明式事件处理器中的错误。如果你使用 addEventListener 添加的事件处理器抛出错误,errorCallback 不会收到通知。
感谢阅读本指南。如需继续学习,请参阅下一章:第三方 Web Components。