跳至主要內容
Skip to content

JS 實戰 (4) —— Web Components:打造原生響應式 UI 元件

雖然現代開發大多擁抱 Vue 或 React,但 Web 標準從未停下腳步。Web Components 是一套由瀏覽器原生支援的組件化技術。它不依賴任何第三方庫,卻能實現高度封裝的 UI。

今天,我們將結合前幾篇學到的 Accessor (Get/Set),來解決 Web Components 最核心的挑戰:數據響應與屬性反射 (Property-Attribute Reflection)


一、 Web Components 的三大支柱

在動手之前,我們必須認識這三位老朋友:

  1. Custom Elements (自定義元素):讓你可以定義像 <my-card> 這樣的標籤。
  2. Shadow DOM (影子 DOM):實現真正的 CSS 與 DOM 封裝,內部樣式不會洩漏到外部。
  3. HTML Templates:定義不會在加載時立即渲染的結構。

二、 核心挑戰:Attribute (特徵) vs Property (屬性)

在 HTML 中,<input value="123"> 裡的 valueAttribute(字串)。在 JS 中,input.value 則是 Property(可能是任意型別)。

在原生開發中,這兩者並不會自動同步。這就是 Getter/Setter 大顯身手的地方。


三、 實戰演練:一個響應式的「進度條」組件

我們希望打造一個 <my-progress> 組件,當我們在 JS 中修改它的百分比時,畫面會自動更新。

javascript
class MyProgress extends HTMLElement {
  constructor() {
    super();
    // 1. 開啟影子 DOM
    this.attachShadow({ mode: "open" });
    this._percent = 0; // 內部的倉庫 (Backing Field)
    this._render();
  }

  // 2. 核心:實現數據反射 (Reflection)
  get percent() {
    return this._percent;
  }

  set percent(val) {
    // 數據驗證
    const constrainedVal = Math.min(100, Math.max(0, Number(val)));
    if (this._percent === constrainedVal) return;

    this._percent = constrainedVal;

    // 同步到 Attribute (這樣 CSS 也可以讀到)
    this.setAttribute("percent", constrainedVal);

    // 更新畫面
    this._updateUI();
  }

  // 3. 監聽 Attribute 變化 (實現反向反射)
  static get observedAttributes() {
    return ["percent"];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === "percent" && oldVal !== newVal) {
      this.percent = newVal; // 觸發 setter
    }
  }

  _render() {
    this.shadowRoot.innerHTML = `
      <style>
        .bar { width: 100%; height: 20px; background: #eee; border-radius: 10px; overflow: hidden; }
        .inner { height: 100%; background: var(--ui-primary, #4ade80); transition: width 0.3s; }
      </style>
      <div class="bar">
        <div class="inner" style="width: ${this._percent}%"></div>
      </div>
    `;
  }

  _updateUI() {
    const inner = this.shadowRoot.querySelector(".inner");
    if (inner) inner.style.width = `${this._percent}%`;
  }
}

customElements.define("my-progress", MyProgress);

四、 為什麼要用 Accessors 處理 Web Components?

  1. 類型安全:Attribute 永遠是字串,透過 Setter 你可以將其轉換為 Number、Boolean。
  2. 邏輯觸發:修改屬性的同時,可以觸發 UI 重新渲染或發送自定義事件 (CustomEvent)。
  3. 封裝性:外部使用者只需要像操作一般 DOM 一樣 el.percent = 50,不需要了解內部的 Shadow DOM 結構。

五、 實戰建議:何時該用 Web Components?

  • 跨框架需求:如果你的組件要在 React 公司與 Vue 公司之間共用。
  • 基礎 UI 庫:按鈕、輸入框、彈窗等高度通用的基礎原子。
  • 微前端架構:作為不同子應用間的通訊媒介與沙箱環境。

️ 進階挑戰

  1. 實作挑戰:為上面的 MyProgress 加入一個 color 屬性。當使用者修改顏色時,進度條的顏色應自動變化。
  2. 深度思考:為什麼 Web Components 的 attributeChangedCallback 需要搭配一個靜態的 observedAttributes 清單?直接監聽所有的 Attribute 不好嗎?

延伸閱讀與資源

返回專題首頁