自定义元素

共 21 题
📑 题目列表 21 题
#
★★★

1. CustomElementRegistry.define 与 is: 属性的内置元素扩展工程价值

请解释 CustomElementRegistry.define 方法的作用,以及使用 is: 属性扩展内置元素的机制,并说明它们在工程中的价值?

  • customElements.define 的注册参数与全局注册表
  • 内置元素扩展(Customized Built-in Elements)的 is: 属性用法
  • 两种方式在工程中的适用场景与兼容性取舍

customElements.define(tagName, class, options) 将自定义元素类注册到全局 customElements 注册表,其中 tagName 必须含连字符。第三种可选参数 options 里的 extends 字段允许将自定义元素类应用于既有内置元素(如 extends: 'ul'),使用时通过 <ul is="my-list"> 声明。这属于"内置元素扩展"(Customized Built-in Elements),与自主元素(Autonomous Custom Elements)相对。工程价值在于:当需要复用一个内置元素固有的语义、无障碍角色与默认行为(例如 ul/li 的列表语义、button 的键盘行为)时,通过 is: 扩展可以保留这些原生能力,只叠加自定义逻辑,避免重新实现整套语义。

define 是 Web Components 三大支柱之一"自定义元素"的注册入口,全局注册表意味着同一名字只能注册一次,重复注册会抛错。is: 扩展在语义保留上更有优势,但浏览器兼容性欠佳(Safari 不支持),因此工程上常需降级为自主元素或通过 polyfill 处理。

customElements.define('my-list', class MyList extends HTMLUListElement {
  connectedCallback() { this.classList.add('custom'); }
}, { extends: 'ul' });
// 使用:<ul is="my-list">...</ul>
#
★★★

2. slot 的分发机制(具名/默认插槽与 slotchange 事件)在自定义元素组合的工程实践

请说明 Shadow DOM 中具名插槽与默认插槽的分发机制,以及 slotchange 事件在自定义元素组合时的工程实践?

  • 与默认插槽的匹配规则
  • slotchange 事件的触发时机与监听
  • 插槽内容与阴影树的组合(composition)关系

在自定义元素的 Shadow DOM 中, 元素是插入点,将外部(light DOM)内容投射到阴影树中。具名插槽通过 <slot name="x"> 匹配带 slot="x" 属性的子元素,未匹配具名插槽的子元素落入默认插槽(无 name 的 )。插槽内容在 DOM 中仍属于 light DOM,属"投射"而非"移动"。slotchange 事件在插槽的分配节点发生变化(首次分配、增删、重排)时触发,且不可冒泡,需在插槽上直接监听。工程实践上,组件常用 slotchange 感知插槽内容变化以更新内部状态或布局,例如 tabs 组件根据 slot 数量生成导航。

这是 Web Components 组合(composition)的核心,与普通 DOM 子树的差异在于插槽内容同时存在于 light DOM 与视觉上的阴影树中,开发者需理解"渲染位置"与"DOM 归属"分离。slotchange 不冒泡是常见陷阱,需在具体 slot 上监听。

class MyCard extends HTMLElement {
  constructor() {
    super();
    const root = this.attachShadow({ mode: 'open' });
    root.innerHTML = `
      <header><slot name="title">默认标题</slot></header>
      <main><slot></slot></main>
    `;
    const titleSlot = root.querySelector('slot[name="title"]');
    titleSlot.addEventListener('slotchange', () => {
      console.log('标题插槽内容变化');
    });
  }
}
#
★★★

3. Constructable Stylesheets 与 Form-associated 自定义元素的协作

请说明 Constructable Stylesheets(可构造样式表)如何与关联表单的自定义元素协作,以及这种协作的工程价值?

  • CSSStyleSheet 构造与 adoptedStyleSheets 机制
  • Form-associated 自定义元素的样式注入
  • 多实例间样式复用与特性检测

Constructable Stylesheets 允许通过 new CSSStyleSheet() 构造样式表,并用 document.adoptedStyleSheetsshadowRoot.adoptedStyleSheets 将其应用到文档或影子根。相较传统在 shadowRoot 内用

协作价值在于"共享但隔离":adoptedStyleSheets 让样式表在多个 Shadow DOM 间复用,同时 Shadow DOM 边界仍保证样式不泄漏到外部。需注意 adoptedStyleSheets 在早期 Safari 支持受限,需特性检测或降级。

const sheet = new CSSStyleSheet();
sheet.replaceSync(':host { display: block; } input { border: 1px solid #ccc; }');

class FancyInput extends HTMLElement {
  static formAssociated = true;
  #internals;
  constructor() {
    super();
    this.#internals = this.attachInternals();
    const root = this.attachShadow({ mode: 'open' });
    root.adoptedStyleSheets = [sheet];
    root.innerHTML = `<input type="text">`;
  }
}
#
★★★

4. Custom Elements 的 customState(:state())与状态查询

请解释 Custom Elements 的 CustomState(ElementInternals.states 与 :state() 伪类)机制,以及它如何用于状态表达与查询?

  • ElementInternals.states 的 CustomStateSet
  • :state() 伪类选择器在样式与外部查询中的应用
  • 相比 data-* 属性或 class 的状态表达优势

通过 this.attachInternals().states 可获取一个 CustomStateSet,它支持集合操作(add/has/delete)来维护元素的自定义状态标识。这些状态可用 CSS 伪类 :state(状态名) 在外部选择,例如 x-tabs:state(active) 可根据内部状态给元素加样式。与以往用 data-* 属性或动态 class 表达状态不同,CustomState 把状态生命周期与元素绑定,语义更清晰、可被外部样式引擎查询,且不与属性反射耦合。该机制能强化封装:组件内部状态可向外暴露为可查询状态,而不必暴露内部实现。

这是较新的规范,工程价值在于为"内部状态"提供标准化的外部可见接口,使主题与样式系统能基于语义状态而非脆弱的 class 约定来定制。需注意兼容性,处于较新阶段,需配合 polyfill 或降级方案。

class Tabs extends HTMLElement {
  #internals;
  constructor() {
    super();
    this.#internals = this.attachInternals();
  }
  activate() {
    this.#internals.states.add('active');
  }
  deactivate() {
    this.#internals.states.delete('active');
  }
}
// CSS: x-tabs:state(active) { border-color: blue; }
#
★★★

5. 自定义元素 constructor 的约束(super 先于 this、禁止读 attributes/children、不可抛错)对框架封装、SSR 与测试的影响

请说明自定义元素 constructor 必须遵守的约束(super 先于 this、禁止读 attributes/children、不可抛错),并分析其对框架封装、SSR 与测试的影响?

  • constructor 中 super() 必须先于 this 访问
  • 禁止读取 attributes/children 与文档操作
  • 不可抛错与相关规范要求

自定义元素 constructor 有严格约束:必须先调用 super() 才能访问 this;constructor 中不允许读取或写入 attributes、添加子元素、设置属性,因为此时元素尚未完成解析,attribute 与 children 尚未就绪;也不应抛错,否则文档解析会被中断。因此初始化逻辑应放入 connectedCallback 或 attributeChangedCallback。对框架封装的影响:框架封装自定义元素时不能让 constructor 依赖 DOM 状态,需把初始化推迟到连接回调。对 SSR 的影响:SSR 产物中无 constructor 执行时机保证,需避免在 constructor 做副作用。对测试的影响:测试中 new 元素后需手动执行升级与连接,或等待 upgrade,才能断言状态。

这些约束由规范强制,原因是元素在 HTML 解析过程中实例化,constructor 执行时 DOM 尚未完整。理解这些约束能避免"constructor 读属性返回 null"等经典陷阱,也解释了为何多数 Web Components 框架把渲染逻辑放在 connectedCallback 或自定义的 attribute 变化回调中。

class MyEl extends HTMLElement {
  constructor() {
    super();             // 必须先执行
    // this.setAttribute('x','1'); // 禁止:constructor 中不可设属性
    // 初始化放这里:创建 shadow root
    this.attachShadow({ mode: 'open' });
  }
  connectedCallback() {
    // 此时才安全读取 attributes / children
    this.render();
  }
}
#
★★★

6. 表单关联自定义元素的 formResetCallback/formDisabledCallback/formStateRestoreCallback 在重置、禁用与状态恢复的应用

请说明表单关联自定义元素的 formResetCallback、formDisabledCallback、formStateRestoreCallback 回调,以及它们在重置、禁用与状态恢复中的应用?

  • 三个回调的触发时机与语义
  • 与 ElementInternals 的协作
  • 表单重置、禁用、状态恢复的工程应用

关联表单的自定义元素(static formAssociated = true)可声明三个生命周期回调:formResetCallback 在所属表单被 reset 时触发,用于把内部值还原为默认;formDisabledCallback 在表单或元素被禁用时触发,用于切换内部控件禁用态;formStateRestoreCallback 在浏览器恢复表单状态(如页面往返缓存 bfcache、导航恢复)时触发,用于恢复之前保存的状态。它们与 ElementInternals 的 setFormValue 协作,让自定义控件真正融入原生表单语义。工程上,这些回调使自定义输入控件在 resets、disabled 与浏览器状态恢复(如刷新/前进后退)场景下行为与原生控件一致,提升可访问性与可预测性。

这是 Form-associated Custom Elements 区别于普通自定义元素的关键能力,让 Shadow DOM 内部控件能参与表单生命周期。实现时需在回调中同步内部 DOM 与 ElementInternals 记录的值,避免表单值与内部显示不一致。

class FancyInput extends HTMLElement {
  static formAssociated = true;
  #internals;
  constructor() { super(); this.#internals = this.attachInternals(); }
  formResetCallback() { this.value = ''; }
  formDisabledCallback(disabled) { this.querySelector('input').disabled = disabled; }
  formStateRestoreCallback(state, reason) {
    this.value = state ?? '';
    this.#internals.setFormValue(this.value);
  }
}
#
★★

7. Autonomous Custom Elements 与 Customized Built-in Elements 的差异

请说明自主自定义元素(Autonomous Custom Elements)与定制内置元素(Customized Built-in Elements)的差异?

  • 两种分类的定义与使用方式
  • 继承的基类差异(HTMLElement vs 特定内置元素)
  • 语义保留、兼容性与工程取舍

自主自定义元素直接继承 HTMLElement,通过新标签名(含连字符)使用,如 <my-button>;定制内置元素继承某个具体内置元素(如 HTMLButtonElement),通过 is: 属性在既有标签上使用,如 <button is="my-button">。差异在于:自主元素完全自建语义与行为,需手工实现角色、焦点与键盘等;定制内置元素继承原元素的语义与无障碍行为,但兼容性差(Safari 不支持 is:),且需通过 options 的 extends 注册。工程取舍上,追求语义与可访问性、目标浏览器兼容时常用自主元素;需要在保留原生语义基础上扩展时考虑内置元素定制,但需注意兼容与 polyfill。

这是自定义元素的两大分类,决定了继承基类、注册方式与语义来源。理解差异有助于在"完全自定义"与"复用原生语义"之间正确选择,避免在 Safari 上使用 is: 造成行为丢失。

// 自主自定义元素
customElements.define('my-button', class extends HTMLElement {});

// 定制内置元素
customElements.define('my-list', class extends HTMLUListElement {}, { extends: 'ul' });
#
★★

8. 自定义元素的样式继承与 Shadow DOM 边界

请说明自定义元素样式通过 Shadow DOM 边界的继承规则,以及 :host 与外部样式的关系?

  • 可继承 CSS 属性跨 Shadow 边界
  • :host 选择器与外部样式优先级
  • 边界上的样式隔离与协作

Shadow DOM 边界在样式上是双向的:外部文档样式通常无法穿透进入影子内部(除非通过 ::part、::slotted 或 CSS 变量),但可继承的 CSS 属性(如 color、font、line-height)会穿过边界影响影子内部。:host 选择器用于给宿主元素本身设置样式,也可被外部样式覆盖(外部针对性更强时优先)。工程上需理解:影子内部默认不受外部标签选择器影响,但继承属性仍会渗透,因此常用 :host 做 reset 或显式继承(如 :host { color: inherit; })来取得协作平衡。

这是 Shadow DOM 封装边界的核心特征之一。样式隔离不是绝对隔离,继承属性会穿透,理解"隔离 vs 继承"的边界才能避免组件内部样式被外部意外影响,或反之组件内外样式不一致。

:host { display: block; color: inherit; }
:host(.highlight) { color: red; }
#
★★

9. CSS Parts(::part/::slotted)的样式定制接口在组件库主题与定制边界的工程价值

请说明 CSS Parts(::part 与 ::slotted)作为样式定制接口的机制,以及它在组件库主题与定制边界的工程价值?

  • ::part() 暴露内部命名部件
  • ::slotted() 选择投射的插槽内容
  • 组件库主题定制与封装边界的平衡

CSS Parts 为 Shadow DOM 的样式定制提供受控接口:组件内部可用 part="name" 标记命名部件,外部通过 ::part(name) 精确选择该部件赋予样式,从而在不破坏封装的前提下实现定制;::slotted(selector) 用于选择投射进插槽的 light DOM 内容。工程价值在于:组件库开发者无需把所有内部样式都开放为主题变量,而是通过 part 暴露有限的定制点,用户可定制指定部件而不影响内部其余部分,从而在"封装隔离"与"可定制性"之间取得平衡,比 CSS 变量更细粒度、比完全穿透更可控。

这是 Shadow DOM 样式隔离与可定制性的平衡点。相较于 ::deep 无条件穿透,::part 是显式、受控的定制接口,是组件库主题系统的现代做法。需注意 ::part 无法匹配内部更深层元素,需组件显式暴露。

<!-- 组件内部 -->
<button part="button icon">...</button>
/* 外部主题 */
my-button::part(button) { border-radius: 8px; }
my-button::part(icon) { width: 16px; }
#
★★

10. 自定义元素的生命周期,connectedCallback 等回调时机?

请说明自定义元素生命周期回调(connectedCallback、disconnectedCallback、adoptedCallback、attributeChangedCallback)的触发时机?

  • 各回调的触发时机与顺序
  • 与 DOM 插入/移除/升级的关系
  • 工程中正确使用各回调的位置

自定义元素的核心生命周期回调包括:connectedCallback 在元素被插入文档(或从 shadow 中插入文档)时触发,是初始化副作用、绑定监听、渲染内容的推荐位置;disconnectedCallback 在元素从文档移除时触发,用于清理监听器与定时器;adoptedCallback 在元素被 document.adoptNode 跨文档转移时触发;attributeChangedCallback 在 observedAttributes 中列出的属性变化(含初始升级时)时触发。理解时机很重要:connectedCallback 可能多次触发(移除再插入),不应在其中做幂等性差的操作;disconnectedCallback 不保证在页面卸载时触发。

生命周期是自定义元素行为的基础。规范强调 connectedCallback 可被多次调用,因此在其中注册监听需考虑重复绑定问题;disconnectedCallback 用于对称清理,但卸载场景可能不触发,需配合 window unload 或 bfcache 回调应对。

class MyEl extends HTMLElement {
  connectedCallback() { this._onClick = () => this.hit(); this.addEventListener('click', this._onClick); }
  disconnectedCallback() { this.removeEventListener('click', this._onClick); }
  adoptedCallback() { console.log('adopted'); }
  attributeChangedCallback(name, oldV, newV) { this.render(); }
  static get observedAttributes() { return ['value']; }
}
#
★★

11. ElementInternals 的 setFormValue/formData API 的工程价值

请说明 ElementInternals 的 setFormValue 与 formData API 在表单关联自定义元素中的工程价值?

  • setFormValue 提交值设定
  • 参与 FormData 构建与表单提交
  • 与 setValidity 协作的校验体系

ElementInternals 通过 attachInternals() 获取,为表单关联自定义元素提供与原生表单集成的接口。setFormValue(value, state) 设置元素将随表单提交的值,使自定义控件能像原生 input 一样参与 FormData 构建与提交;还可通过作为 FormData 构造器参数(如 new FormData(form))被收集。setValidity 提供校验能力,设置 validity 状态并配合 :invalid 伪类。工程价值在于:让 Shadow DOM 内部的自定义输入控件无缝融入原生表单体系,无需手动拦截 submit 事件,同时获得校验、FormData 收集、表单状态等能力,显著提升互操作性。

这是 Form-associated Custom Elements 的核心,使自定义控件成为"一等公民"表单控件。setFormValue 的第二个参数是用于状态恢复的 state,与 formStateRestoreCallback 配合。理解这些 API 才能构建真正可用的表单控件组件。

class FancyInput extends HTMLElement {
  static formAssociated = true;
  #internals;
  constructor() { super(); this.#internals = this.attachInternals(); }
  set value(v) {
    this.#internals.setFormValue(v);
    this.#internals.setValidity(v ? {} : { valueMissing: true }, '请输入值');
  }
  get value() { return this.#internals.formValue ?? ''; }
}
#
★★

12. Custom Elements 的 upgrade 机制(已有元素实例的升级)

请说明自定义元素的 upgrade 机制,即 HTML 中已存在的元素实例如何被"升级"为自定义元素?

  • 解析器立即升级 vs 延迟升级
  • customElements.upgrade 与 whenDefined
  • 升级对外观与行为的影响

当浏览器解析 HTML 遇到未知标签时,会先创建未知元素(HTMLUnknownElement 或普通元素),若之后 customElements.define 注册了对应类,已有实例会被"升级":调用 constructor 与 connectedCallback,继承相应类。解析器已存在的元素会升级;通过 customElements.upgrade(root) 可强制升级某子树内未升级的元素;customElements.whenDefined(name) 返回 Promise,在元素被定义后 resolve,便于等待升级完成。工程价值:支持组件库异步加载,元素先渲染成占位,类加载后再升级为完整组件,避免阻塞首屏。

升级机制让"先有 DOM 后定义类"成为可能,是异步组件加载与渐进增强的基础。升级时若元素已在文档中会触发 connectedCallback;若尚未插入文档则不会触发,待之后插入文档时再触发。whenDefined 常用于等待依赖组件就绪。

customElements.whenDefined('x-y').then(() => {
  console.log('x-y 已定义');
});
// 强制升级某子树
customElements.upgrade(document.getElementById('app'));
#
★★

13. observedAttributes 与属性变更回调

请说明 observedAttributes 静态属性与 attributeChangedCallback 回调的机制,以及属性反射的工程实践?

  • observedAttributes 声明监听属性
  • attributeChangedCallback 触发时机
  • attribute 与 property 反射

自定义元素通过静态 getter observedAttributes 返回要监听的属性名字符串数组,浏览器据此在属性变化(setAttribute/removeAttribute)时调用 attributeChangedCallback(name, oldValue, newValue)。属性反射指把 property 与 attribute 双向同步:setAttribute 触发属性回调或在 property setter 中 setAttribute,使状态在两种表示间保持一致。工程实践上,属性变化回调常用于驱动内部渲染,而 property 是 JS 侧更高效的访问方式,attribute 是 HTML 声明式与外部工具(如框架绑定)的接口。需注意:仅仅 render 需要属性,removeAttribute 时 newValue 为 null。

这是自定义元素响应外部状态变化的关键。observedAttributes 只影响 attributeChangedCallback 触发,不自动把 attribute 同步到 property,需开发者手动实现反射。区分 attribute(字符串、HTML 语义)与 property(任意类型、JS 语义)是重要考点。

class MyEl extends HTMLElement {
  static get observedAttributes() { return ['value', 'disabled']; }
  attributeChangedCallback(name, oldV, newV) {
    if (name === 'value') this.#value = newV;
    this.render();
  }
  get value() { return this.#value; }
  set value(v) { this.#value = v; this.setAttribute('value', v); } // 反射
}
#
★★

14. Shadow DOM 的封装边界(open/closed)与事件重定向(composed/composedPath)在自定义元素的应用

请说明 Shadow DOM 的 open/closed 模式与事件重定向(composed、composedPath)机制,以及它们在自定义元素中的应用?

  • attachShadow mode: open/closed 差异
  • 事件重定向与 composedPath
  • composed 标志与跨边界事件

attachShadow({mode}) 的 mode 决定封装边界:open 模式使 shadowRoot 可通过 element.shadowRoot 访问,元素的状态几乎可被外部脚本读取;closed 模式返回 null,外部无法直接访问 shadowRoot,但规范不保证绝对安全,且 many 实现会克制。事件重定向指从 Shadow DOM 内部触发的事件在冒泡到文档时,其 target 会被重定向为宿主元素(除非未 composed);composed 标志决定事件能否跨过 Shadow 边界继续冒泡,默认 false 的普通事件不越过边界,composed: true 的自定义事件可冒泡到文档。composedPath() 返回事件经过的真实路径(含 shadow 内部节点)。工程上,自定义元素用 composed 让应用层能监听内部事件,用 composedPath 进行事件溯源。

这是封装与通信的平衡。closed 模式带来局部安全但伤害可测试性与可调试性,多数组件库用 open。理解事件重定向避免"内部事件 target 是宿主"的困惑,是自定义元素事件编程的重难点。

this.dispatchEvent(new CustomEvent('internal', { composed: true, bubbles: true }));
document.addEventListener('internal', (e) => {
  console.log(e.composedPath()); // 含 shadow 内部节点
});
#
★★

15. Lit Element / FAST Element 在轻量 Web Components 框架的工程取舍

请比较 Lit Element 与 FAST Element 作为轻量 Web Components 框架在工程上的取舍?

  • Lit 的响应式属性与模板系统
  • FAST 的模板与元素工厂
  • 两者在体积、生态、工程适配的取舍

Lit 是 Google 推出的轻量 Web Components 框架,基于访问器(getter/setter)与更新循环的响应式属性,配合 template literal 的 html 模板,reactive properties 声明 reactive(attribute 反射、缓存、变更触发更新),css 辅助函数支持 CSS 标签模板,内置 repeat、classMap、styleMap 等指令。FAST 是 Microsoft 的框架,提供 FASTElement 与基于指令的模板系统,同样编译为自定义元素。工程取舍:Lit 生态更成熟、文档完善、与 React/Vue 集成文档丰富,是当前 Web Components 框架的主流选择;FAST 在模板编译与设计系统(如 Microsoft 的 Fluent UI)上有优势但生态相对小。选型需综合团队熟悉度、生态、体积与维护活跃度。

两者都产出标准自定义元素,可跨框架复用,本质是"框架无关的组件库"方案。取舍点主要在响应式实现、模板语法、指令能力与生态成熟度,而非是否能跨框架。

#
★★

16. 自定义元素默认 display: inline 与 HTML 解析器对未知元素的处理(块级布局、自闭合差异)在模板复用时的陷阱

请说明自定义元素默认 display: inline 与 HTML 解析器对未知元素(块级布局、自闭合差异)的处理,以及这些在模板复用中的陷阱?

  • 自定义元素默认 display: inline
  • HTML 解析器对未知/自闭合元素的处理
  • 模板复用时的布局陷阱

自定义元素默认 display 为 inline(与未知元素一样),因此若期望块级布局或需设置宽高,需在 :host 中显式设置 display: block。HTML 解析器对未知元素按普通元素处理,且 HTML 中自闭合标签(如 )不会真正自闭合,会被当作标签开始,后续内容会被当作其子元素,与 XML 的差异造成模板复用陷阱。工程上复用模板时,若把自定义元素写成自闭合形式,会导致后续内容被错误包含,破坏布局与结构。需显式提供闭合标签,并在样式层为宿主元素声明 display。

display:inline 默认值常被忽略,是"自定义元素宽高不生效"的常见原因。自闭合差异源于 HTML 解析规则(非 XML),slot 与模板复用场景尤其容易踩坑,理解解析器行为是正确书写模板的前提。

:host { display: block; }
<!-- 错误:HTML 中不会被当作自闭合 -->
<my-el/>
<!-- 正确 -->
<my-el></my-el>
#
★★

17. 自定义元素默认无角色与键盘焦点行为,role、tabindex 与焦点管理的手动补齐,及与原生控件语义的差距

请说明自定义元素默认无角色与键盘焦点行为,以及如何通过 role、tabindex 与焦点管理手动补齐,并分析与原生控件的语义差距?

  • 自定义元素默认无 ARIA 角色与键盘交互
  • role、tabindex 的手动补齐
  • 焦点管理(focus 委托、焦点陷阱)

自定义元素默认是普通元素,无特定 ARIA 角色、无键盘交互语义、无 tabindex 加入 tab 序。要实现可访问的组件,需手动补齐:设置 role(如 button、tab、checkbox)、设置 tabindex(focusable 元素设 0,焦点管理元素设 -1 以编程聚焦)、实现键盘事件(Enter/Space 触发、方向键导航)、管理焦点(focus 委托到内部 focusable 元素、焦点陷阱)。即便如此,与原生控件(如原生 button/input)相比仍有差距:原生控件自带语义、键盘、焦点环与表单行为,自定义元素需全部手工实现且易遗漏。工程上用 focusable 内部元素委托焦点、配合 aria-* 属性提升可访问性。

这是"自定义元素的默认能力缺失"问题,也是无障碍测试常抓的重点。补齐 role/tabindex/键盘语义是构建可访问组件库的基本功,理解与原生控件差距有助于决定是否用 is: 扩展或原生封装。

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.setAttribute('role', 'button');
    this.tabIndex = 0;
    this.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') this.click();
    });
  }
}
#

18. Custom Elements 经历了哪些关键版本演进,与 Lit、Fast、Shoelace 等框架/库相比各自适用什么场景

请说明 Custom Elements 规范的关键演进阶段,并比较其与 Lit、Fast、Shoelace 等框架/库的适用场景?

  • Custom Elements 规范的演进历程
  • 原生 Custom Elements 与 Lit/Fast 的定位
  • Shoelace 等组件库的定位

Custom Elements 规范(Custom Elements v1)取代了 v0 的自动升级与自定义标签机制,成为标准,与 Shadow DOM、HTML Templates 构成 Web Components 三大支柱;后续演进包括 form-associated custom elements、ElementInternals、customState、Declarative Shadow DOM 等。原生 Custom Elements 直接面向 Web 标准,适合需要零依赖、跨框架复用的基础组件;Lit 提供响应式属性与模板语法,适合快速构建组件库、减少样板代码;Fast 提供模板与设计系统支撑;Shoelace 是基于 Lit 的现成组件库,适合直接使用成熟组件而不自研。选型依据:零依赖追求用原生,自研组件库用 Lit/Fast,直接使用组件用 Shoelace 等。

演进主线是"标准能力增强 + 框架生态成熟"。原生规范解决"能不能",Lit/Fast 解决"好不好写",Shoelace 解决"用现成"。理解各自定位才能在架构选型时正确决策。

#

19. Custom Elements 与 design system(Material Web / Shoelace / Spectrum Web Components)

请说明 Custom Elements 在设计系统(如 Material Web、Shoelace、Spectrum Web Components)中的应用?

  • 各设计系统基于 Web Components 的实现
  • 跨框架复用与主题定制
  • 选择 Web Components 设计系统的考量

多个主流设计系统基于 Web Components 实现:Material Web Components 是 Google 用 Lit 实现 Material Design 的组件库;Shoelace 是基于 Lit 的独立组件库,强调主题定制与易用;Spectrum Web Components 是 Adobe 用 Lit 实现其设计语言。这些库以标准自定义元素交付,可在任何框架(React/Vue/Angular)中直接使用,实现在不改变框架栈的前提下统一设计系统。工程上选择 Web Components 设计系统需权衡:跨框架复用与框架无关性是优势,但需注意事件绑定、属性传递等框架集成细节,以及样式定制接口(CSS 变量、::part)的成熟度。

设计系统选用 Web Components 的核心理由是"一次实现、处处可用"。成功与否取决于主题系统(CSS 变量/Part)与框架集成(props/events/slots)的成熟度,这也是评估标配。

#

20. Shadow DOM 的样式隔离,与全局样式的边界?

请说明 Shadow DOM 的样式隔离机制,以及它与全局样式的边界?

  • 样式隔离的基本规则
  • 外部样式与继承属性的穿透
  • :host、::part、::slotted 的边界接口

Shadow DOM 提供样式隔离:外部文档中的样式选择器(如标签选择器、类选择器)默认无法匹配并作用到影子内部节点,反之影子内部样式也不会泄漏到外部文档。但边界并非完全封闭:可继承的 CSS 属性(color、font 等)会从宿主穿透进入影子内部;:host 可设置宿主样式并被外部样式覆盖;::part、::slotted 提供受控的定制接口;CSS 自定义属性(变量)会沿继承链穿透。工程上理解边界是:默认隔离避免全局样式污染组件,同时通过继承属性、CSS 变量与 Part 提供受控干预通道,实现"隔离为主、协作为辅"。

样式隔离是 Shadow DOM 的重大价值,让组件样式自包含。但"边界"是双向有孔洞的(继承、变量、part),准确理解这些孔洞才能既保持封装又支持主题定制。

#

21. 自定义元素命名必须含连字符与保留标签约束、重复 define 抛错的注册冲突治理

请说明自定义元素命名的约束(必须含连字符、保留标签)与重复 define 抛错问题,以及注册冲突的治理?

  • 命名必须含连字符
  • 保留标签与内置元素名称冲突
  • 重复 define 抛错与冲突治理

规范要求自定义元素标签名必须包含连字符(-),以区分于内置元素与未来可能的扩展,避免与现有 HTML 元素冲突;同时存在保留标签(如 annotation-xml、font-face 等 SVG/MathML 名称)不可用作自定义元素名。同名元素重复调用 customElements.define 会抛错(NotSupportedError),因为注册表是全局单例。治理策略:组件库统一命名前缀(如 my-)避免撞名;用命名空间或构建时前缀注入避免多库冲突;对重复注册做防御性检查(try/catch 或检查 customElements.get 是否已存在);在微前端场景用 Scoped Element Registries 让多版本共存。

命名规则是防止自定义元素与内置/未来元素冲突的基础。全局注册表导致重复 define 抛错,是组件库与微前端必须处理的冲突源,治理核心是命名空间隔离与防御性检查。

if (!customElements.get('my-el')) {
  customElements.define('my-el', class extends HTMLElement {});
}