JavaScript 中的 Shadow DOM

JavaScript 中的 Shadow DOM

本文將解釋 JavaScript 中的 Shadow DOM。

YouTube Video

javascript-html-shadow-dom.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    h1, h2 {
 32        font-size: 1.2rem;
 33        color: #007bff;
 34        margin-top: 0.5em;
 35        margin-bottom: 0.5em;
 36        border-left: 5px solid #007bff;
 37        padding-left: 0.6em;
 38        background-color: #e9f2ff;
 39    }
 40
 41    button {
 42        display: block;
 43        margin: 1em auto;
 44        padding: 0.75em 1.5em;
 45        font-size: 1rem;
 46        background-color: #007bff;
 47        color: white;
 48        border: none;
 49        border-radius: 6px;
 50        cursor: pointer;
 51        transition: background-color 0.3s ease;
 52    }
 53
 54    button:hover {
 55        background-color: #0056b3;
 56    }
 57
 58    #output {
 59        margin-top: 1em;
 60        background-color: #1e1e1e;
 61        color: #0f0;
 62        padding: 1em;
 63        border-radius: 8px;
 64        min-height: 200px;
 65        font-family: Consolas, monospace;
 66        font-size: 0.95rem;
 67        overflow-y: auto;
 68        white-space: pre-wrap;
 69    }
 70
 71    .highlight {
 72        outline: 3px solid #ffc107; /* yellow border */
 73        background-color: #fff8e1;  /* soft yellow background */
 74        transition: background-color 0.3s ease, outline 0.3s ease;
 75    }
 76
 77    .active {
 78        background-color: #28a745; /* green background */
 79        color: #fff;
 80        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 81        transition: background-color 0.3s ease, box-shadow 0.3s ease;
 82    }
 83  </style>
 84</head>
 85<body>
 86    <div class="container">
 87        <h2>HTML Sample</h2>
 88        <div id="content"></div>
 89        <div id="shadow-host">Shadow Root Element</div>
 90        <my-card></my-card>
 91    </div>
 92
 93    <div class="container">
 94        <h1>JavaScript Console</h1>
 95        <button id="executeBtn">Execute</button>
 96        <div id="output"></div>
 97    </div>
 98
 99    <div>
100        <h2>Slot Sample</h2>
101        <my-element>
102            <h3 slot="header">Header Content</h1>
103            <p slot="content">Main Content</p>
104        </my-element>
105    </div>
106
107    <script>
108        // Override console.log to display messages in the #output element
109        (function () {
110            const originalLog = console.log;
111            console.log = function (...args) {
112                originalLog.apply(console, args);
113                const output = document.getElementById('output');
114                output.textContent += args.map(String).join(' ') + '\n';
115            };
116        })();
117
118        document.getElementById('executeBtn').addEventListener('click', () => {
119            // Prevent multiple loads
120            if (document.getElementById('externalScript')) return;
121
122            const script = document.createElement('script');
123            script.src = 'javascript-html-shadow-dom.js';
124            script.id = 'externalScript';
125            //script.onload = () => console.log('javascript-html-shadow-dom.js loaded and executed.');
126            //script.onerror = () => console.log('Failed to load javascript-html-shadow-dom.js.');
127            document.body.appendChild(script);
128        });
129    </script>
130</body>
131</html>

了解 Shadow DOM

Shadow DOM 是 Web 組件標準的一項強大功能,允許在組件內部封裝樣式和 DOM 結構。此功能可防止組件與主文檔之間的樣式和腳本相互干擾。

JavaScript 中的 Shadow DOM

Shadow DOM 提供了一種為常規 DOM 元素創建作用域 DOM 樹的方法。該 Shadow 樹與整個文檔相隔離,外部樣式和腳本無法影響它,而其內部樣式和腳本也不會洩漏出去。

例如,使用 Shadow DOM 創建自定義按鈕組件時,其樣式不會干擾頁面上的其他元素。同樣,具有相同 class 名稱的元素也不會產生衝突。

Shadow DOM 之外的一般 HTML 內容稱為 Light DOM

Shadow DOM 的優勢

  1. 封裝性

    • Shadow DOM 將樣式與功能進行隔離,防止與全域樣式和腳本衝突。
  2. 可重複使用性

    • Shadow DOM 建立的元件可以在不同專案中重複使用,無需擔心樣式衝突。
  3. 維護性

    • 封裝性讓元件的邏輯和樣式獨立存在,使除錯和維護更加容易。

創建 Shadow DOM

要使用 Shadow DOM,需要將一個 shadow 根附加到 HTML 元素上。以下是一個簡單範例:。

1// Select the host element
2const hostElement = document.querySelector('#shadow-host');
3
4// Attach a shadow root
5const shadowRoot = hostElement.attachShadow({ mode: 'open' });

解釋

這段程式碼包含以下元素:。

  1. 主機元素(Host Element)

    • 附加 shadow root 的一般 DOM 元素(此例中為 #shadow-host)。
  2. Shadow Root

    • 使用 attachShadow 所建立的 shadow tree 根節點。
  3. 模式(Mode)

    • open 模式下,外部 JavaScript 可以透過 element.shadowRoot 存取 shadow root。另一方面,closed 模式不允許訪問。

Shadow DOM 中的樣式處理

Shadow DOM 有自己獨立的樣式範圍。在 shadow tree 中定義的樣式僅適用於該樹中的元素。以下是一個範例:。

1// Add content to the shadow root
2shadowRoot.innerHTML = `
3    <style>
4        p {
5            color: green;
6        }
7    </style>
8    <p>Scoped style inside Shadow DOM.</p>
9`;

即使主文件中有衝突的樣式,也不會影響 shadow tree 中的段落。

1const content = document.getElementById('content');
2content.innerHTML = `
3    <style>
4        p {
5            color: red;
6        }
7    </style>
8    <p>This is in the main DOM.</p>
9`;
  • Shadow DOM 中的段落仍為綠色,而外部段落則為紅色。

Shadow DOM 中的事件

Shadow DOM 中的事件類似於普通的 DOM 事件,但由於封裝的原因,在傳播方面可能表現不同。

以下是一個範例:。

 1// Add an event listener inside Shadow DOM
 2shadowRoot.innerHTML = `
 3    <div id="button-container">
 4        <button id="shadow-button">Click Me</button>
 5    </div>
 6`;
 7
 8shadowRoot.querySelector('#shadow-button').addEventListener('click', (event) => {
 9    console.log('Button : Button clicked inside Shadow DOM');
10    console.log(event.target);
11});
12
13shadowRoot.querySelector('#button-container').addEventListener('click', (event) => {
14    console.log('Container : Button clicked inside Shadow DOM');
15    console.log(event.target);
16});
17
18hostElement.addEventListener('click', (event) => {
19    console.log('Event bubbled to the host element');
20    console.log(event.target);
21});
22
23document.addEventListener('click', (event) => {
24    console.log('Document listener');
25    console.log(event.target);
26});
  • 當按鈕被點擊時,兩個監聽器都會被觸發,展示了事件冒泡的行為。
  • 當來自 Shadow DOM 內部的事件冒泡到 Light DOM 時,該事件的 target 會被改寫為宿主元素,而不是原始來源元素。
    • 在此範例中,Shadow DOM 內的 event.target 是實際的 button 元素,但在 Shadow DOM 之外,它會被替換為宿主的 div 元素。

Shadow DOM 與自訂元素

Shadow DOM 和自訂元素是 Web Components 的關鍵組件。它們是用於建立可重複使用且具封裝性的 UI 元件的技術。

 1class MyCard extends HTMLElement {
 2    constructor() {
 3        super();
 4        const shadow = this.attachShadow({ mode: 'open' });
 5        shadow.innerHTML = `
 6        <style>
 7            p {
 8                color: blue;
 9            }
10        </style>
11        <p>I'm inside shadow DOM</p>
12        `;
13    }
14}
15
16customElements.define('my-card', MyCard);

主文件包含如下 HTML:。

1<my-card></my-card>
  • 透過在自訂元素內使用 Shadow DOM,你可以建立能避免樣式衝突的可重複使用元件。在這段程式碼中,創建了一個名為 my-card 的標籤,並將其關聯到 MyCard 類別。my-card 內的 <p> 元素不會受到外部樣式的影響,並且永遠以藍色顯示。

Slots:分配 Light DOM 內容

Slots 可讓你將 Light DOM 內容投影到 Shadow DOM 中。以下是一個範例:。

 1class MyElement extends HTMLElement {
 2    constructor() {
 3        super();
 4        const shadow = this.attachShadow({ mode: 'open' });
 5
 6        shadow.innerHTML = `
 7        <style>
 8            .container {
 9                border: 2px solid #ccc;
10                padding: 16px;
11                border-radius: 8px;
12                font-family: sans-serif;
13            }
14            .header {
15                font-size: 1.2em;
16                color: darkblue;
17                margin-bottom: 8px;
18            }
19            .content {
20                font-size: 1em;
21                color: #333;
22            }
23        </style>
24        <div class="container">
25            <div class="header">
26                <slot name="header"></slot>
27            </div>
28            <div class="content">
29                <slot name="content"></slot>
30            </div>
31        </div>
32        `;
33    }
34}
35
36customElements.define('my-element', MyElement);

主文件包含如下 HTML:。

1<my-element>
2    <h3 slot="header">Header Content</h1>
3    <p slot="content">Main Content</p>
4</my-element>
  • Shadow DOM 內的 slot 元素會顯示具有對應 slot 屬性的 Light DOM 內容。

結論

Shadow DOM 是構建穩健、可重用且易於維護的網頁組件的重要工具。通過封裝樣式和功能,它減少了衝突的可能性並簡化了代碼庫的管理。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video