跳至主要內容

Web Components 原生元件開發完全指南

Web Components 原生元件開發完全指南

在現代前端開發中,React、Vue、Angular 等框架幾乎佔據了主導地位。然而,有一種被許多人忽視的技術——Web Components——它是瀏覽器原生支援的元件化解決方案,不依賴任何框架,跨框架可用,且擁有極佳的長期穩定性。

什麼是 Web Components?

Web Components 是一組瀏覽器 API 的集合,主要由三個核心技術組成:

  1. Custom Elements:允許定義自訂 HTML 元素
  2. Shadow DOM:提供封裝的 DOM 和樣式
  3. HTML Templates:可重複使用的 HTML 片段

Custom Elements 基礎

建立一個自訂元素非常直觀:

class MyButton extends HTMLElement {
  constructor() {
    super();
    this._count = 0;
  }

  connectedCallback() {
    this.render();
    this.querySelector('button').addEventListener('click', () => {
      this._count++;
      this.render();
    });
  }

  render() {
    this.innerHTML = `
      <button style="padding: 8px 16px; cursor: pointer;">
        點擊次數:${this._count}
      </button>
    `;
  }
}

customElements.define('my-button', MyButton);

生命週期回呼

Custom Elements 提供幾個重要的生命週期方法:

class MyComponent extends HTMLElement {
  connectedCallback() {
    console.log('元素已連接到 DOM');
  }

  disconnectedCallback() {
    console.log('元素已從 DOM 移除');
  }

  adoptedCallback() {
    console.log('元素已移至新文件');
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`屬性 ${name} 從 ${oldValue} 變更為 ${newValue}`);
  }

  static get observedAttributes() {
    return ['color', 'size', 'disabled'];
  }
}

Shadow DOM 封裝

Shadow DOM 是 Web Components 最強大的特性之一,它能讓元件的樣式完全隔離:

class StyledCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    const style = document.createElement('style');
    style.textContent = `
      :host { display: block; font-family: sans-serif; }
      .card {
        border: 1px solid #e2e8f0;
        border-radius: 8px;
        padding: 16px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      }
      .card-title { font-size: 1.25rem; font-weight: bold; margin-bottom: 8px; }
    `;

    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = `
      <div class="card-title"><slot name="title">預設標題</slot></div>
      <div class="card-body"><slot>預設內容</slot></div>
    `;

    shadow.appendChild(style);
    shadow.appendChild(card);
  }
}

customElements.define('styled-card', StyledCard);

使用方式:

<styled-card>
  <span slot="title">我的卡片標題</span>
  <p>這是卡片的主要內容。</p>
</styled-card>

HTML Templates

使用 <template> 標籤可以定義不會立即渲染的 HTML 片段:

<template id="user-card-template">
  <style>
    .user-card { display: flex; align-items: center; gap: 12px; }
    .avatar { width: 48px; height: 48px; border-radius: 50%; }
    .info .name { font-weight: bold; }
    .info .email { color: #666; font-size: 0.875rem; }
  </style>
  <div class="user-card">
    <img class="avatar" src="" alt="Avatar">
    <div class="info">
      <div class="name"></div>
      <div class="email"></div>
    </div>
  </div>
</template>
class UserCard extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('user-card-template');
    const clone = template.content.cloneNode(true);
    clone.querySelector('.avatar').src = this.getAttribute('avatar') || '';
    clone.querySelector('.name').textContent = this.getAttribute('name') || '';
    clone.querySelector('.email').textContent = this.getAttribute('email') || '';
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(clone);
  }
}
customElements.define('user-card', UserCard);

與 Vue 3 整合

// main.js
import { createApp } from 'vue';
const app = createApp(App);
app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-');
app.mount('#app');

實際應用:通知元件

class NotificationToast extends HTMLElement {
  static get observedAttributes() {
    return ['type', 'message', 'duration'];
  }

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

  connectedCallback() {
    this.render();
    const duration = parseInt(this.getAttribute('duration') || '3000');
    if (duration > 0) setTimeout(() => this.remove(), duration);
  }

  render() {
    const message = this.getAttribute('message') || '';
    this.shadowRoot.innerHTML = `
      <style>
        .toast {
          padding: 12px 16px;
          border-left: 4px solid #63b3ed;
          background: #ebf8ff;
          color: #2c5282;
          border-radius: 4px;
          font-family: sans-serif;
          font-size: 14px;
          animation: slideIn 0.3s ease;
        }
        @keyframes slideIn {
          from { transform: translateX(100%); opacity: 0; }
          to   { transform: translateX(0); opacity: 1; }
        }
      </style>
      <div class="toast">${message}</div>
    `;
  }
}
customElements.define('notification-toast', NotificationToast);

瀏覽器支援

目前主流瀏覽器(Chrome、Firefox、Safari、Edge)均完整支援 Web Components 三大核心 API。

何時使用 Web Components?

適合使用的場景:

  • 跨框架的設計系統(如 Adobe Spectrum、Shoelace)
  • 嵌入第三方頁面的元件
  • 需要長期維護且不想被框架版本綁定的元件庫
  • 微前端架構中的共享元件

總結

Web Components 提供了一種原生、標準化的元件化方案。掌握它能讓你對瀏覽器原生能力有更深入的理解,在跨框架共用元件的場景中,它是無可替代的解決方案。

分享這篇文章