组件无障碍访问与生命周期 — LWC Accessibility 完全指南

LWC 无障碍与生命周期完整参考。涵盖无障碍属性(label/aria-label/setAttribute 反射)、ARIA 高级属性(camelCase 映射/默认值/静态值)、跨模板 ID 链接(Light DOM 方案)、焦点处理(tabindex 0/-1/delegatesFocus)、完整生命周期(constructor 规范/connectedCallback 多次触发/disconnectedCallback 清理/renderedCallback 无限循环避免/async 钩子反模式/render 条件模板/errorCallback 错误边界与声明式 vs 编程式事件限制)。...

📅 2026/7/19 ✍️ ponybai 🏷️ lwc, salesforce, accessibility

组件无障碍访问

组件无障碍访问

无障碍软件和辅助技术使残障用户也能使用你构建的产品。开发组件时应确保所有用户都能感知、理解、导航和交互。基础组件提供内置的无障碍支持;自定义组件需自行实现。推荐遵循 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 属性

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 导航

浏览器完全可通过键盘 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

constructor 在组件实例创建时触发,是第一个执行的生命周期钩子。流方向:父→子(父组件先构造,子组件后构造——因为子组件还没被创建)。

此时不可用的内容:不能访问子元素(还不存在);不能访问通过模板渲染的元素(this.template 尚不可用);不能检查元素的属性(attributes)和子元素(children);公共属性也尚未赋值——它们在 constructor 之后、connectedCallback 之前才被设置。

HTML Custom Elements 规范五条强制规则

  1. 首条语句必须是无参 super():建立正确的原型链和 this 值。始终在访问 this 之前调用 super()
  2. 不使用 return 语句:除非是简单的提前返回(returnreturn this
  3. 不使用 document.write() 或 document.open()
  4. 不检查元素的属性和子元素:此时它们还不存在
  5. 不检查公共属性:它们在组件创建后才设置

常见错误 —— 在构建期间添加宿主元素属性

// ❌ 绝对不要这样做
constructor() {
  super();
  this.classList.add("new-class");  // 元素尚未在 DOM 中!
}

// ✅ 正确做法 —— 在 connectedCallback 中
connectedCallback() {
  this.classList.add("new-class");  // 元素已在 DOM 中
}

你可以在除 constructor 外的任何生命周期阶段向宿主元素添加属性。constructor 的最佳用途仅限于:初始化简单的类字段默认值、设置非响应式状态。所有其他操作——尤其是任何涉及 DOM 或属性的——都应该延迟到 connectedCallback() 中执行。

connectedCallback() 与 disconnectedCallback()

connectedCallback

这两个钩子遵循 Web Components 标准。流方向都是父→子。使用 this 访问宿主元素,this.template 访问组件模板中的元素。你还可使用 this.isConnected 检查组件是否连接到 DOM。

connectedCallback()

组件插入 DOM时触发。可用于:

  • 与当前文档或容器建立通信,协调行为
  • 执行初始化任务——获取数据、设置缓存、监听事件
  • 订阅/取消订阅消息频道(Message Channel)
  • 使用 lightning/navigation 模块导航
  • 使用第三方 Web Components

三个关键警告:

  1. 可能触发多次!例如从 DOM 中移除后重新插入(如列表重排序)。每次插入都会触发。如果代码应该只运行一次,使用守卫标志防止重复执行。
  2. 无法访问子元素!此时子组件的 connectedCallback 尚未执行——this.template.querySelector("div") 返回 null。不能依赖子组件的 DOM 结构。
  3. 属性赋值时机:属性在 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

renderedCallback 是 LWC 独有的钩子——在组件完成渲染阶段后执行逻辑。流方向子→父(子组件先完成渲染,父组件后完成——因为父组件的渲染包含子组件的渲染结果)。

渲染与重渲染机制

组件连接并渲染后,任何状态变更触发重渲染流程:

  1. 组件被标记为 "dirty"
  2. 一个执行重渲染的 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 方法

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。