自定义元素比class封装更适合作为组件库底层,因其注册真实dom节点,语义明确、无障碍友好、属性变更自动触发回调,且shadow dom提供样式隔离与结构保护,而class仅是普通标签无法表达组件契约。

为什么自定义元素比 class 封装更适合作为组件库底层
因为 customElements.define() 注册的是真实 DOM 节点,不是字符串类名或 JS 对象。浏览器解析时直接实例化,无障碍工具、搜索引擎、Lighthouse 都能识别语义,而 <button class="ds-button"></button> 只是普通 <button></button>,无法表达“这是一个设计系统按钮”的契约。
常见错误现象:用 CSS 类封装的“组件”在 axe 扫描中不被识别为可交互控件;设计师标注的 <ds-input></ds-input> 在代码里写成 <input class="ds-input" ...>,导致属性绑定失效、焦点管理错乱、禁用态逻辑无法统一控制。
- 自定义元素天然支持
aria-属性透传,且role、tabindex等行为由浏览器原生接管 - 属性变更触发
attributeChangedCallback,无需手动监听dataset或轮询 - 标签名含连字符(如
ds-button)是强制要求,否则customElements.define()直接抛DOMException
Shadow DOM 是设计系统隔离性的物理前提
不是“可选增强”,而是防止样式穿透的唯一可靠机制。BEM、CSS Modules、CSS-in-JS 都无法阻止 body * { font-size: 12px } 这类全局规则影响子组件字体大小。
使用场景:当多个业务方共用同一套设计系统时,某一方升级了全局 reset.css,所有未用 Shadow DOM 的组件立即视觉错位。
-
this.attachShadow({ mode: 'closed' })比'open'更安全,外部 JS 无法访问内部 DOM,避免意外篡改 -
<slot></slot>是唯一受控的内容注入点,业务方传入的<span slot="label">确认</span>不会破坏组件结构 - Shadow DOM 内部的
<style></style>不参与全局级联,也不被外部!important覆盖
customElements.define() 必须在首次渲染前执行
如果脚本延迟加载或动态 import 组件模块,<ds-button></ds-button> 标签已被 HTML 解析器当作 HTMLUnknownElement 处理,后续注册无法“复活”其行为——它永远只是个空壳节点。
容易踩的坑:把注册逻辑放在 DOMContentLoaded 之后,或包裹在某个条件判断里(如仅在管理后台加载),导致首页首屏渲染失败。
- 推荐做法:在模块顶层立即执行
customElements.define(),不要延迟 - 幂等性必须由库自身保证,重复调用会抛
NotSupportedError,可用if (!customElements.get('ds-button')) { ... }包裹 - SSR 场景下需确保服务端不执行该方法,只在客户端注册
自定义内置元素(extends)适合渐进式迁移
当你已有大量 <button></button>、<input> 使用,又想统一接入设计系统行为时,customElements.define('ds-button', DsButton, { extends: 'button' }) 允许你保留原有标签结构,只需加 is="ds-button"。
参数差异:{ extends: 'button' } 必须配合继承 HTMLButtonElement,不能继承 HTMLElement;否则注册失败且无明确报错。
- HTML 中写法是
<button is="ds-button" variant="primary">提交</button>,不是<ds-button></ds-button> - 这种写法仍能通过
form.elements被表单收集,保持原生表单语义 - 但无法使用
<slot></slot>,内容渲染需靠this.innerHTML或this.textContent控制
最易被忽略的点:自定义元素不是“高级语法糖”,它是设计系统交付的最终接口。一旦发布 ds-button,它的属性名、事件名、插槽名就构成契约——改一个 size 属性为 scale,所有业务方都得同步改,没有框架层的兼容层兜底。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











