Web Components 深度解析:Shadow DOM、Custom Elements 与浏览器原生组件化

前端工程(更新于 2026年6月1日)

Web Components:浏览器原生的组件化方案

Web Components 是 W3C 推出的浏览器原生组件化标准,由三个核心 API 组成:

API 作用 状态
Custom Elements 定义新 HTML 标签 稳定,全浏览器支持
Shadow DOM 封装样式与 DOM 稳定,全浏览器支持
HTML Templates 声明可复用 DOM 片段 稳定,全浏览器支持

另有 CSS Parts::part())用于穿透 Shadow DOM 样式定制。


Custom Elements:定义你的 HTML 标签

生命周期回调

class MyTooltip extends HTMLElement {
  static get observedAttributes() {
    return ['position', 'content'];
  }

  constructor() {
    super();
    console.log('1. constructor — 初始化');
  }

  connectedCallback() {
    console.log('2. connectedCallback — 插入 DOM');
    this.render();
  }

  disconnectedCallback() {
    console.log('3. disconnectedCallback — 移除 DOM');
    this.cleanup();
  }

  attributeChangedCallback(name: string, oldVal: string, newVal: string) {
    console.log(`4. attributeChanged: ${name} ${oldVal} → ${newVal}`);
    if (oldVal !== newVal) this.update(name, newVal);
  }

  adoptedCallback() {
    console.log('5. adoptedCallback — 移入新 document');
  }
}

customElements.define('my-tooltip', MyTooltip);

注册规则

// 标签名必须包含连字符
customElements.define('my-button', MyButton);   // ✅
customElements.define('button', MyButton);       // ❌ 不允许

// 不能重复注册
customElements.define('my-button', MyButton);    // ✅
customElements.define('my-button', MyButton2);   // ❌ 已注册

// 查询是否已注册
const MyBtn = customElements.get('my-button');
const defined = customElements.whenDefined('my-button');
defined.then(() => console.log('my-button 可用了'));

Shadow DOM:样式与结构的封装

开放与封闭模式

class ShadowComponent extends HTMLElement {
  constructor() {
    super();
    // Open:外部可通过 element.shadowRoot 访问
    this.attachShadow({ mode: 'open' });
    // Closed:外部无法访问 shadowRoot
    // this.attachShadow({ mode: 'closed' });
  }
}
特性 open closed
element.shadowRoot 返回 Shadow Root 返回 null
外部查询内部元素 可以 不可以
querySelector 穿透 可以 不可以
实际使用频率 极低

样式封装实战

class StyledCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          border-radius: 8px;
          overflow: hidden;
          background: #fff;
          box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        :host([highlighted]) {
          border: 2px solid #4f46e5;
        }
        ::slotted(h2) {
          margin: 0;
          padding: 16px;
          color: #1f2937;
        }
        .content {
          padding: 0 16px 16px;
        }
      </style>
      <slot name="header"></slot>
      <div class="content">
        <slot></slot>
      </div>
    `;
  }
}
customElements.define('styled-card', StyledCard);

使用:

<styled-card highlighted>
  <h2 slot="header">卡片标题</h2>
  <p>卡片内容,样式完全封装</p>
</styled-card>

CSS Parts:穿透 Shadow DOM 定制

// 组件内部
shadow.innerHTML = `
  <style>
    .title { font-size: 18px; }
  </style>
  <h2 class="title" part="title">
    <slot></slot>
  </h2>
`;
styled-card::part(title) {
  font-size: 24px;
  color: #e11d48;
}

HTML Templates 与 Slot

Template:声明式 DOM 模板

<template id="card-template">
  <style>
    .card { padding: 16px; border: 1px solid #e5e7eb; border-radius: 8px; }
  </style>
  <div class="card">
    <slot name="title"></slot>
    <slot></slot>
  </div>
</template>
class TemplateCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    const template = document.getElementById('card-template') as HTMLTemplateElement;
    shadow.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('template-card', TemplateCard);

Slot 分发机制

<template-card>
  <!-- 具名 Slot -->
  <span slot="title">标题</span>
  <!-- 默认 Slot -->
  <p>内容</p>
</template-card>

与框架组件对比

维度 Web Components React Vue
标准 W3C 标准 社区生态 社区生态
样式封装 Shadow DOM 天然隔离 CSS Modules / CSS-in-JS Scoped CSS
跨框架复用 原生支持 需包装层 需包装层
响应式 需手动(attributeChangedCallback) 自动(setState/hooks) 自动(ref/reactive)
模板 字符串 / Template JSX SFC Template
生态/工具链 较少 极丰富 丰富
SSR 有限支持 完善支持 完善支持
开发体验 偏底层 高层抽象 高层抽象

实战:构建一个评分组件

class StarRating extends HTMLElement {
  private _value = 0;
  private _max = 5;

  static get observedAttributes() {
    return ['value', 'max'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.setupEvents();
  }

  attributeChangedCallback(name: string, _: string, newVal: string) {
    if (name === 'value') this._value = Number(newVal);
    if (name === 'max') this._max = Number(newVal);
    if (this.shadowRoot) this.render();
  }

  private render() {
    const stars = Array.from({ length: this._max }, (_, i) => {
      const filled = i < this._value;
      return `<span class="star ${filled ? 'filled' : ''}" data-index="${i}">★</span>`;
    }).join('');

    this.shadowRoot!.innerHTML = `
      <style>
        :host { display: inline-flex; gap: 4px; cursor: pointer; }
        .star { font-size: 24px; color: #d1d5db; transition: color 0.15s; }
        .star.filled { color: #f59e0b; }
        .star:hover { color: #fbbf24; }
      </style>
      ${stars}
    `;
  }

  private setupEvents() {
    this.shadowRoot!.addEventListener('click', (e) => {
      const target = e.target as HTMLElement;
      if (target.classList.contains('star')) {
        const index = Number(target.dataset.index) + 1;
        this.setAttribute('value', String(index));
        this.dispatchEvent(new CustomEvent('rate', {
          detail: { value: index },
          bubbles: true,
          composed: true
        }));
      }
    });
  }
}
customElements.define('star-rating', StarRating);

使用:

<star-rating value="3" max="5"></star-rating>
<script>
  document.querySelector('star-rating')
    .addEventListener('rate', (e) => {
      console.log('评分:', e.detail.value);
    });
</script>

浏览器兼容性

API Chrome Firefox Safari Edge
Custom Elements v1 54+ 63+ 10.1+ 79+
Shadow DOM v1 53+ 63+ 10.1+ 79+
HTML Templates 35+ 22+ 9+ 79+
CSS ::part() 73+ 72+ 13.1+ 79+
adoptedCallback 73+ 63+ 15.4+ 79+

2026 年主流浏览器均已完整支持,无需 polyfill。


最佳实践

  1. 命名规范:使用 org-component 前缀避免冲突(如 toolsku-color-picker
  2. 优先 open mode:closed mode 调试困难,收益有限
  3. 属性反射:关键属性同步到 DOM attribute,便于 CSS 选择器
  4. 事件 composed:需要穿透 Shadow DOM 的事件设置 composed: true
  5. 懒注册:使用 customElements.whenDefined 管理依赖顺序

总结

Web Components 提供了框架无关的组件化原语,适合构建跨项目复用的 UI 原子组件。虽然开发体验不如 React/Vue 高层抽象,但其样式封装原生标准的优势在微前端、设计系统等场景中不可替代。

推荐使用 代码沙盒 快速验证 Web Components 代码,用 SVG 编辑器 制作组件内嵌图标。

本站提供浏览器本地工具,免注册即可试用 →

#Web Components#Shadow DOM#Custom Elements#组件化#浏览器原生