iT邦幫忙

2025 iThome 鐵人賽

DAY 4
1
Modern Web

原生元件養成計畫:Web Component系列 第 4

Day 4: Web Component 中的影子樹 Shadow DOM

  • 分享至 

  • xImage
  •  

接觸到 Web Component 才第一次聽過的名詞,花了一些時間才真正理解用法。
就請各位看下去吧!

Shadow DOM 的基本概念

影子 DOM(Shadow DOM)允許你將一個 DOM 樹附加到一個元素上,並且使該樹的內部對於在頁面中運行的 JavaScript 和 CSS 是隱藏的。 --MDN

一般的 DOM

一般的 DOM 就是 HTML 嗎?
其實 HTML 本身沒有太大的用處,但是瀏覽器將靜態的 HTML 解析成資料模型(物件、節點),而這些節點包含了 屬性方法,讓我們可以透過 JS 來訪問這些節點,進行存取、新增、刪除或更改內容。
而以上提到的內容指的就是一般的 DOM。

Shadow DOM

Shadow DOM 是附著在某個元素上的 獨立 DOM 樹,是一種解決一般 DOM 中缺少的 樹封裝 方法。
其實在開發過程中我們常常會遇到需要寫 !important 來覆蓋某些衝突的樣式,而使用 Shadow DOM API,可以透過 封裝 DOM 樹 的機制來解決 CSS 和 JavaScript 的衝突問題,裡面的結構與樣式不會影響到外部,也不會受到外部影響。
shadow-dom
(圖片中的用語 light DOM 其實就是一般的 DOM,主要用來跟 shadow DOM 做區分)

  • Shadow host:就是作為 Shadow root 的代管容器,也就是掛載 Shadow DOM 的元素。(Shadow host 也可以是 Shadow DOM 中的元素)
  • Shadow root:shadow 樹中最頂層的節點,是在創建 shadow DOM 時附加到 DOM 節點的內容。

加入 Shadow DOM 的好處與壞處有什麼?

好處:

  1. 樣式封裝
    • 元素內的 CSS 不會影響到外部元素,外部 CSS 也不會影響 Shadow DOM 裡的內容。
    • 可以透過 CSS Custom Properties 提供修改樣式的入口
  2. DOM 結構封裝
    • 外部無法透過一般的 querySelector 找到 Shadow DOM 內的元素

壞處:

  1. 樣式難以被外部覆蓋
    • 樣式封裝有好處也有壞處,由於樣式封裝的關係,導致無法直接透過外部給予 css 就覆蓋樣式(難以客製化樣式)。可能會讓樣式的整合困難性提高,像我們現在常常使用 tailwind,就很難與位於 Shadow 中的元件整合。
  2. 學習成本、debug 難度提高
    • 在 dev mode 中,我們需要從 #shadow-root 找到元素,結構比普通 DOM 更複雜。

如何建立 Shadow DOM

要建立 Shadow DOM,需要用 Element.attachShadow() 的方法將 shadow root 附加到元素上,而 Element 就是 Shadow host。

    Element.attachShadow({ mode: "open" | "closed" })

注意用法:

  1. 部分元素(<video>, <img> ...)已有內建 shadow DOM,不允許再疊加。
  2. 某些結構元素或關鍵元素(<table><html> ...) 已有內建行為,不可作為 Shadow DOM。
  3. 只開放給 自定義元素 與部分安全、結構簡單的元素(div, span, p, section...)。
const div = document.createElement('div');
const shadowRoot = header.attachShadow({mode: 'open'}); // 合法的用法

試試看:嘗試將 shadow DOM 附加到不合法的元素將會導致 NotSupportedError 錯誤

document.createElement('input').attachShadow({mode: 'open'}); // 非法的用法
// Uncaught NotSupportedError

attach-shadow

若要在 Chrome 中顯示此類元素的 Shadow root,可以在 DevTools 設定,然後在 elements 部分下方選取 Show user agent shadow DOM

參考連結:https://dom.spec.whatwg.org/#shadow-trees

為我們建立的元件加入 Shadow DOM

前一篇我們利用基本的 customElements.define 建立了一個自定元件 cat-spinner
接下來要加入 Shadow DOM,先建立一個 shadowRoot 變數。

class CatSpinner extends HTMLElement {
  constructor() {
    super();

    // 還記得 this 吧?不記得要回去讀一下基礎的 JS 囉!
    // 先建立一個 shadowRoot 變數
    const shadowRoot = this.attachShadow({ mode: 'open' });

    shadowRoot.innerHTML = `
      <style>
        .spinner {
          width: 40px;
          height: 40px;
          border: 4px solid #ddd;
          border-top-color: #333;
          border-radius: 50%;
          animation: spin 1s linear infinite;
        }
        @keyframes spin {
          to { transform: rotate(360deg); }
        }
      </style>
      <div class="spinner"></div>
    `;
  }
}
customElements.define('cat-spinner', CatSpinner);

打開 Dev Tool 看一下:
你會發現,cat-spinner 作為 Shadow host,加入了 Shadow root 在裡面。

shadow-root

attachShadow 中的 mode

open 模式:

  • 訪問性:可以從外部透過 JavaScript 訪問 Shadow Root
  • 獲取方式:可以通過 element.shadowRoot 屬性獲取 Shadow Root,並且取得內部節點。
  • 使用時機:需要外部 JavaScript 與 Shadow DOM 互動時使用(像是按鈕、input...)

closed 模式:

  • 訪問性:無法從外部透過 JavaScript 訪問 Shadow Root
  • 獲取方式:element.shadowRoot 返回 null
  • 使用時機:需要完全封裝內部程式邏輯,不希望外部修改(像是密碼強度驗證器...)
const element = document.querySelector('cat-spinner');

console.log('open mode', element.shadowRoot)   // 會得到 #shadow-root (open)
console.log('closed mode', element.shadowRoot) // 會得到 null

關於 Shadow DOM 的介紹就到這裡啦ヾ(´∀ ˋ)ノ


上一篇
Day 3: 初步設計與 Custom element
下一篇
Day 5: Web Component 中的模板 Template
系列文
原生元件養成計畫:Web Component6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言