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。
最佳實踐
- 命名規範:使用
org-component前綴避免衝突(如toolsku-color-picker) - 優先 open mode:closed mode 除錯困難,收益有限
- 屬性反射:關鍵屬性同步到 DOM attribute,便於 CSS 選擇器
- 事件 composed:需要穿透 Shadow DOM 的事件設定
composed: true - 懶註冊:使用
customElements.whenDefined管理依賴順序
總結
Web Components 提供了框架無關的元件化原語,適合建構跨專案複用的 UI 原子元件。雖然開發體驗不如 React/Vue 高層抽象,但其樣式封裝和原生標準的優勢在微前端、設計系統等場景中不可替代。
本站提供瀏覽器本地工具,免註冊即可試用 →
#Web Components#Shadow DOM#Custom Elements#组件化#浏览器原生