Web Components 是浏览器原生标准 API,由 Custom Elements、Shadow DOM 和 HTML Templates 组成;需继承 HTMLElement、用 customElements.define() 注册含短横线的标签名,并通过 observedAttributes 和 attributeChangedCallback 响应属性变化。

Web Components 是什么?它不是框架,而是浏览器原生能力
Web Components 不是某个库或工具链,它是浏览器内置的一套标准 API 集合,由 Custom Elements、Shadow DOM 和 HTML Templates 三部分组成。其中最直接支撑“可复用自定义元素”的,就是 CustomElementRegistry.define() —— 它让你注册一个带行为的 HTML 标签,比如 <my-button>,之后就能像 <div> 一样在任何地方使用。
如何用 class 定义一个可复用的自定义元素?
必须继承 HTMLElement,并在构造函数中初始化 DOM(注意:不能在这里调用 this.innerHTML 或访问 this.children,因为此时元素尚未挂载);生命周期钩子如 connectedCallback 才是插入 Shadow DOM 和绑定事件的安全时机。
class MyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'closed' });
const template = document.createElement('template');
template.innerHTML = `
<style>button { background: #007bff; color: white; border: none; padding: 8px 16px; }</style>
<button><slot></slot></button>
`;
shadow.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('my-click', { bubbles: true }));
});
}
}
customElements.define('my-button', MyButton);
-
attachShadow({ mode: 'closed' })阻止外部 JS 访问 Shadow DOM,提升封装性 -
<slot>是内容分发点,允许用户写<my-button>点击我</my-button> - 必须用
customElements.define()注册,且标签名中必须含短横线(-),否则会报Failed to execute 'define' on 'CustomElementRegistry': The name provided must contain a dash
为什么不用 innerHTML 直接写 Shadow DOM?
因为 innerHTML 会忽略 <style> 标签的 scoped 行为,也无法自动解析 <slot>。更关键的是:直接赋值字符串无法触发浏览器对内联样式和模板的优化路径,且易引入 XSS(除非你严格 sanitize)。用 template + cloneNode(true) 是标准做法,它保证样式隔离、内容插槽生效,并被所有现代浏览器正确识别。
- 如果漏掉
template.content.cloneNode(true),多次实例化时会复用同一份节点,导致事件重复绑定或状态污染 -
mode: 'closed'下,this.shadowRoot在外部为null,调试时别误以为没创建成功 - 不支持 IE,Edge 18 及以下也不行;若需兼容,必须加 polyfill(如 @webcomponents/webcomponentsjs)
属性变更如何响应?得靠 static get observedAttributes()
自定义元素不会自动监听属性变化。要让 my-button disabled 或 my-button size="large" 触发逻辑更新,必须显式声明观察列表,并在 attributeChangedCallback 中处理。
立即学习“Java免费学习笔记(深入)”;
class MyButton extends HTMLElement {
static get observedAttributes() {
return ['disabled', 'size'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
this.shadowRoot.querySelector('button').disabled = newValue !== null;
}
if (name === 'size' && newValue === 'large') {
this.shadowRoot.querySelector('button').style.padding = '12px 24px';
}
}
}
注意:attributeChangedCallback 不会在元素首次初始化时触发(即使 HTML 中已写属性),只响应后续 JS 修改或 setAttribute 调用。初始状态得在 connectedCallback 或构造函数里手动同步一次。
真正难的不是写出来,而是想清楚哪些状态该暴露为属性、哪些该用 slot 分发、哪些该走事件通信——这些设计决策一旦定下,后续组件组合就很难推翻。











