Shadow Root 是附加至主機元素的封裝 DOM 子樹:內部的樣式不會外洩,頁面樣式也不會洩漏 (繼承屬性除外)。範本可有效建構樹狀結構,而版位則可將 Light DOM 內容投影到樹狀結構中。
附加 Shadow Root
this.attachShadow({ mode: 'open', delegatesFocus: true });
delegatesFocus: true 會將焦點移至 Shadow Tree 內第一個可聚焦的元素,將 :focus 樣式轉送至主體,並在表單相關聯的自訂元素 (static formAssociated = true) 上,將 <label> 點擊轉送至內部控制項。只要元件包裝可聚焦的控制項,就應設定此屬性。在絕大多數情況下,請使用 mode: 'open',讓開發人員工具、網頁指令碼和伺服器凍結功能可以存取 element.shadowRoot (請參閱網頁元件簡介)。
範本:有效率地建構樹狀結構
<template> 只需定義一次 (模組範圍或靜態欄位),然後為每個執行個體複製。複製剖析過的片段,比每次建構時重新剖析 innerHTML 字串快得多。
const template = document.createElement('template');
template.innerHTML = `
<div class="card">
<slot name="header"></slot>
<div class="body"><slot></slot></div>
<slot name="footer"></slot>
</div>
`;
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// MANDATORY: deep-clone the template content so each instance gets its own nodes.
this.shadowRoot.append(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
保持範本精簡。深度巢狀結構的「div soup」和多層巢狀 Shadow Root 都會增加可測量的樣式解析和版面配置成本,對大規模效能造成重大影響。建議採用平面結構和語意元素。
插槽:令人驚喜的部分
預設和已命名版位的運作方式與說明文件所述相同,但以下行為可能會造成問題:
- 空白字元會計入字數。元素之間的文字節點 (包括純空白字元) 會指派給預設版位,因此預設版位很少會真正「空白」。
- 系統會保留 DOM 順序,而非版位順序。無論投影節點填入哪個版位,都會保留原始 Light DOM 順序,以利
:first-child/:last-child。::slotted(*:first-child)規則會指定第一個 DOM 子項,而不是該版位的第一個子項,因此通常不會與您預期的相符。 slotchange是升級時機信號。當指派的節點變更 (包括初始指派) 時,就會觸發這個事件,這也是在升級後得知 Light DOM 子項已到達的可靠方式,因為在connectedCallback期間可能不存在。
this.shadowRoot.querySelector('slot[name="header"]')
.addEventListener('slotchange', (e) => {
const nodes = e.target.assignedElements();
this.toggleAttribute('has-header', nodes.length > 0);
});
將樣式表載入 Shadow Root
| 方法 | 優點 | 缺點 |
|---|---|---|
範本中的 <style> |
簡單、完全封裝,不需 JS 即可運作。 | 每個執行個體都會重複,大規模時會導致 DOM 膨脹。 |
<link rel="stylesheet"> |
可快取的外部檔案。 | 額外要求;可能導致未套用樣式的內容閃現。 |
adoptedStyleSheets |
所有執行個體共用一個 CSSStyleSheet,大規模運作時速度最快。 |
需要 JS;建構的工作表不支援 @import。 |
如果一個元件有多個例項,建議使用共用的建構樣式表:
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// The same sheet object is reused by every instance; no per-instance parse.
this.shadowRoot.adoptedStyleSheets = [sheet];
}
}