Shadow DOM、範本和版位

網頁功能: Shadow DOM <slot>

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];
  }
}