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();
    this.attachShadow({ mode: 'open' });
    // 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);

與框架元件對比

維度 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#组件化#浏览器原生