什么是javascript的Web Components_它如何创建可复用的自定义元素?

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

Web Components 是什么?它不是框架,而是浏览器原生能力

Web Components 不是某个库或工具链,它是浏览器内置的一套标准 API 集合,由 Custom ElementsShadow DOMHTML Templates 三部分组成。其中最直接支撑“可复用自定义元素”的,就是 CustomElementRegistry.define() —— 它让你注册一个带行为的 HTML 标签,比如 ,之后就能像 一样在任何地方使用。

如何用 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 = `
      
      
    `;
    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,提升封装性
  • 是内容分发点,允许用户写 点击我
  • 必须用 customElements.define() 注册,且标签名中必须含短横线(-),否则会报 Failed to execute 'define' on 'CustomElementRegistry': The name provided must contain a dash

为什么不用 innerHTML 直接写 Shadow DOM?

因为 innerHTML 会忽略 标签的 scoped 行为,也无法自动解析 。更关键的是:直接赋值字符串无法触发浏览器对内联样式和模板的优化路径,且易引入 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 disabledmy-button size="large" 触发逻辑更新,必须显式声明观察列表,并在 attributeChangedCallback 中处理。

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 分发、哪些该走事件通信——这些设计决策一旦定下,后续组件组合就很难推翻。