影子 DOM 用於封裝。它允許元件擁有自己的「影子」DOM 樹,無法從主文件意外存取,可能有本機樣式規則,以及更多功能。
內建影子 DOM
你是否曾想過複雜的瀏覽器控制項是如何建立和設定樣式的?
例如 <input type="range">
瀏覽器在內部使用 DOM/CSS 來繪製它們。該 DOM 結構通常對我們隱藏,但我們可以在開發人員工具中看到它。例如在 Chrome 中,我們需要在開發人員工具中啟用「顯示使用者代理影子 DOM」選項。
然後 <input type="range">
會像這樣
你在 #shadow-root
下看到的是「影子 DOM」。
我們無法透過常規的 JavaScript 呼叫或選擇器取得內建影子 DOM 元素。這些不是常規的子項,而是一種強大的封裝技術。
在上面的範例中,我們可以看到一個有用的屬性 pseudo
。它是非標準的,存在於歷史原因。我們可以使用它來使用 CSS 設定子元素樣式,如下所示
<style>
/* make the slider track red */
input::-webkit-slider-runnable-track {
background: red;
}
</style>
<input type="range">
再次強調,pseudo
是一個非標準屬性。依時間順序來說,瀏覽器首先開始嘗試使用內部 DOM 結構來實作控制項,然後,隨著時間推移,Shadow DOM 被標準化,允許我們開發人員執行類似的事情。
接下來,我們將使用現代 Shadow DOM 標準,由 DOM 規範 和其他相關規範涵蓋。
Shadow tree
一個 DOM 元素可以有兩種 DOM 子樹
- Light tree – 一個常規 DOM 子樹,由 HTML 子項組成。我們在前面章節中看到的所有子樹都是「Light」。
- Shadow tree – 一個隱藏的 DOM 子樹,不會反映在 HTML 中,不會被窺探到。
如果一個元素同時擁有這兩種子樹,則瀏覽器只會渲染 Shadow tree。但是,我們也可以設定 Shadow tree 和 Light tree 之間的一種組合。我們將在章節 Shadow DOM slots, composition 中看到詳細資訊。
Shadow tree 可以用於自訂元素,以隱藏組件內部結構並套用組件本機樣式。
例如,這個 <show-hello>
元素會將其內部 DOM 隱藏在 Shadow tree 中
<script>
customElements.define('show-hello', class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<p>
Hello, ${this.getAttribute('name')}
</p>`;
}
});
</script>
<show-hello name="John"></show-hello>
這是 Chrome 開發人員工具中顯示的 DOM 結果,所有內容都位於「#shadow-root」之下
首先,呼叫 elem.attachShadow({mode: …})
會建立一個 Shadow tree。
有兩個限制
- 我們只能為每個元素建立一個 Shadow root。
elem
必須是自訂元素,或下列元素之一:「article」、「aside」、「blockquote」、「body」、「div」、「footer」、「h1…h6」、「header」、「main」、「nav」、「p」、「section」或「span」。其他元素,例如<img>
,無法承載 Shadow tree。
mode
選項設定封裝層級。它必須具有下列兩個值之一
-
"open"
– Shadow root 可用於elem.shadowRoot
。任何程式碼都可以存取
elem
的 Shadow tree。 -
"closed"
–elem.shadowRoot
永遠是null
。我們只能透過
attachShadow
傳回的參照存取 Shadow DOM(而且可能隱藏在類別中)。瀏覽器原生 Shadow tree,例如<input type="range">
,是封閉的。無法存取它們。
attachShadow
所傳回的 影子根節點 就如同一個元素:我們可以使用 innerHTML
或 DOM 方法(例如 append
)來填充它。
具有影子根節點的元素稱為「影子樹宿主」,並可作為影子根節點的 host
屬性取得
// assuming {mode: "open"}, otherwise elem.shadowRoot is null
alert(elem.shadowRoot.host === elem); // true
封裝
影子 DOM 與主文件有嚴格的區隔
- 影子 DOM 元素對於來自光 DOM 的
querySelector
是不可見的。特別是,影子 DOM 元素的 id 可能與光 DOM 中的 id 衝突。它們必須在影子樹中保持唯一性。 - 影子 DOM 有自己的樣式表。外部 DOM 的樣式規則不會套用。
例如
<style>
/* document style won't apply to the shadow tree inside #elem (1) */
p { color: red; }
</style>
<div id="elem"></div>
<script>
elem.attachShadow({mode: 'open'});
// shadow tree has its own style (2)
elem.shadowRoot.innerHTML = `
<style> p { font-weight: bold; } </style>
<p>Hello, John!</p>
`;
// <p> is only visible from queries inside the shadow tree (3)
alert(document.querySelectorAll('p').length); // 0
alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
- 文件中的樣式不會影響影子樹。
- …但內部的樣式會生效。
- 若要取得影子樹中的元素,我們必須從樹內部查詢。
參考資料
- DOM:https://dom.spec.whatwg.org/#shadow-trees
- 相容性:https://caniuse.dev.org.tw/#feat=shadowdomv1
- 許多其他規格中都有提到影子 DOM,例如 DOM Parsing 規定影子根節點具有
innerHTML
。
摘要
影子 DOM 是一種建立元件區域 DOM 的方法。
shadowRoot = elem.attachShadow({mode: open|closed})
– 為elem
建立影子 DOM。如果mode="open"
,則可作為elem.shadowRoot
屬性取得。- 我們可以使用
innerHTML
或其他 DOM 方法來填充shadowRoot
。
影子 DOM 元素
- 有自己的 id 空間,
- 對於主文件中的 JavaScript 選擇器(例如
querySelector
)是不可見的, - 僅使用影子樹中的樣式,不使用主文件中的樣式。
如果存在影子 DOM,瀏覽器會渲染它,而不是所謂的「光 DOM」(一般子元素)。在 影子 DOM 插槽、組成 章節中,我們將了解如何組成它們。
留言
<code>
標籤,若要插入多行程式碼,請將其包覆在<pre>
標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)