Shadow DOM v1 - 自封式網頁元件

Shadow DOM 可讓網頁開發人員為網頁元件建立區隔式 DOM 和 CSS

摘要

Shadow DOM 讓建構網頁應用程式再也不必費心。極地 來自全球的 HTML、CSS 和 JS。多年來,我們 發明瞭一個超長的數字 / 工具 規避問題例如,使用新的 HTML ID/類別時 因此無法判斷這會與網頁使用的名稱發生衝突。 系統偵測到細微錯誤 CSS 的具體性會成為一大問題 (這全都是 !important、什麼!)、樣式 許多控制項都不在掌控權範圍內 效能可能會受到影響。清單 繼續。

Shadow DOM 修正 CSS 和 DOM。為此,我們針對網路推出範圍樣式。 平台。如果沒有工具或命名慣例,您可以將 CSS 與 標記、隱藏實作詳細資料,作者獨立成 元件

簡介

Shadow DOM 是 Web 元件標準的三項標準之一: HTML 範本Shadow DOM自訂元素HTML 匯入項目 原本就包含在名單中 已淘汰

您不需要編寫使用 shadow DOM 的網頁元件。但當你這麼做 您可以運用這個平台的優點 (CSS 範圍設定、DOM 封裝、 建構可重複使用 自訂元素 彈性、設定高度且可重複使用如為自訂值 元素是建立新 HTML (使用 JS API) 的方式,shadow DOM 是 。這兩個 API 會結合在一起 並搭配獨立的 HTML、CSS 和 JavaScript 使用

Shadow DOM 旨在協助您建構以元件為基礎的應用程式。因此 它帶來網路開發的常見問題解決方案:

  • 獨立 DOM:元件的 DOM 為獨立性質 (例如 document.querySelector() 不會傳回元件的 shadow DOM 中的節點)。
  • 限定範圍的 CSS:在 shadow DOM 內定義的 CSS 範圍限定為該 CSS。樣式規則 請不要洩漏,而且頁面樣式不會出血。
  • 組合:為元件設計宣告式標記式 API。
  • 簡化 CSS - 限定範圍的 DOM 可讓您使用簡單的 CSS 選取器, 通用 ID/類別名稱,無須擔心命名衝突。
  • 效率提升:請將應用程式視為 DOM 區塊,而非大型語言 (全域) 網頁中。
,瞭解如何調查及移除這項存取權。

fancy-tabs 示範

在這篇文章中,我將是指示範元件 (<fancy-tabs>) 並從中參照程式碼片段如果瀏覽器支援 API 應該就能看到這項功能的現場示範否則,請查看 GitHub 上的完整原始碼

前往 GitHub 查看原始碼

什麼是 shadow DOM?

DOM 背景

HTML 是網路的驅動力,因為易於使用。只要宣告幾個標記 而且只要幾秒鐘就能編寫具有呈現和結構的網頁不過 因為 HTML 本身並不是最實用的。對於人類來說,這是一件容易理解的文字- 但機器需要更多機制輸入文件物件 Model,也就是 DOM

瀏覽器載入網頁時,會做許多有趣的事。下列其中一項 它會將作者的 HTML 轉換為即時文件 基本上,為了瞭解網頁結構,瀏覽器會剖析 HTML ( 轉換為資料模型 (物件/節點)。瀏覽器會保留 藉由建立這些節點的樹狀結構,也就是 DOM 的階層結構。酷炫內容 DOM 是網頁即時呈現的內容不同於 我們編寫的 HTML,瀏覽器產生的節點包含屬性、方法,以及最佳 都能透過程式操控!這就是我們能夠建立 DOM 的原因 直接使用 JavaScript 元素:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

會產生下列 HTML 標記:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

一切良好。接著 什麼是 shadow DOM

陰影中的 DOM...

Shadow DOM 是一般的 DOM,有兩個差異:1) 如何建立/使用物件,以及 2) 網頁相對於其他網頁的行為。通常您需要建立 DOM 並附加為另一個元素的子項有了 shadow DOM 建立一個限定範圍的 DOM 樹狀結構,附加至元素,但與元素分開 實際的子女。這種範圍的子樹狀結構稱為「陰影樹狀結構」。元素 附加至用戶端的影子主機。您在陰影中新增的內容 其本機位置,包括 <style>。這就是 shadow DOM 的 會達到 CSS 樣式範圍設定

正在建立 shadow DOM

陰影根是指附加至「主機」元素的文件片段。 附加陰影根目錄的做法,是元素取得陰影 DOM 的方式。目的地: 為元素建立 shadow DOM,並呼叫 element.attachShadow()

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

我使用 .innerHTML 填滿陰影根目錄,但您也可以使用其他 DOM 相互整合也就是網路。我們別無選擇。

這個規格會定義元素清單 無法代管影子樹有幾個原因會導致元素出現 清單:

  • 瀏覽器已為元素代管本身的內部 shadow DOM (<textarea><input>)。
  • 元素要代管陰影 DOM (<img>) 並不合理。

舉例來說,這就無效:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

為自訂元素建立 shadow DOM

陰影 DOM 在建立物件時特別實用 自訂元素。 因此,使用 shadow DOM 將元素的 HTML、CSS 和 JS 加以劃分, 製作「網頁元件」

範例 - 自訂元素將 shadow DOM 附加至本身。 封裝其 DOM/CSS:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

這裡有幾個有趣的事。首先 自訂元素會在 <fancy-tabs> 例項時建立自己的 shadow DOM 也就是在 constructor() 中完成。其次,由於系統將建立 陰影根層級,<style> 內的 CSS 規則的範圍會限定為 <fancy-tabs>

組合和運算單元

組合是 Shad DOM 最容易理解的特徵之一 是最重要的

在網頁程式開發的領域中,合成就是建構應用程式的方式 。不同的建構模塊 (<div><header><form><input>) 會合而為一。部分廣告代碼甚至可以運作 應用程式設計組合是 <select><details><form><video> 非常靈活。每個廣告代碼都接受 就某些 HTML 程式碼來說 很類似例如: <select> 知道如何將 <option><optgroup> 算繪為下拉式選單並 提供多選項小工具。<details> 元素會將 <summary> 算繪為 可展開的箭頭。就連 <video> 也知道如何處理特定子項: <source> 元素不會算繪,但會影響影片行為。 真了不起!

術語:Light DOM 與陰影 DOM

Shadow DOM 組合引入了一系列網路新基礎知識 。進入雜草前吧 因此我們只是想說同樣的術語

Light DOM

元件使用者會寫入標記。這個 DOM 位於 元件的 shadow DOM。這是元素的實際子項。

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

陰影 DOM

元件作者編寫的 DOM。Shadow DOM 是元件的本機位置 定義其內部結構、限定範圍的 CSS,並封裝您的實作 詳細資料。也可定義如何轉譯由消費者編寫的標記 元件

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

壓平合併 DOM 樹狀結構

瀏覽器將使用者的 Light DOM 發布到陰影中的結果 DOM 轉譯最終產品一棵平整的樹木 以及頁面上顯示的內容

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

<slot>元素

陰影 DOM 會使用 <slot> 元素組合不同的 DOM 樹狀結構。 版位是元件中的預留位置,使用者「可以」填入版位 標記。定義一或多個版位後,即可邀請外部標記顯示 嵌入至元件的 shadow DOM 中基本上,您是在說「轉譯使用者的 標記在這裡」

元素可以「交叉」在 <slot> 邀請時加入陰影 DOM 界線 。這些元素稱為「分散式節點」。概念上 分散各地的節點似乎有些奇怪運算單元不會實際移動 DOM;他們 會算繪在 shadow DOM 中的其他位置。

元件可在其 shadow DOM 中定義零個或多個版位。運算單元可留空 或提供備用內容如果使用者未提供 light DOM 內容就會算繪備用內容。

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

您也可以建立已命名的運算單元。已命名的版位是指 使用者由名稱參照的 shadow DOM。

範例 - <fancy-tabs> 的 shadow DOM 中的版位:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

元件使用者宣告 <fancy-tabs> 的方式如下:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

您只要思考一下,平緩的樹看起來會像這樣:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

請注意,我們的元件能夠處理不同的設定,但 扁平化的 DOM 樹狀結構維持不變。我們也可以從 <button> 切換至 <h2>。這個元件的設計是為了處理不同類型的子項... 就像 <select> 確實如此!

樣式

設定網頁元件樣式的選項有很多種。使用陰影的元件 DOM 可由主頁面設定樣式、定義專屬樣式,或提供掛鉤 ( CSS 自訂屬性的形式),方便使用者覆寫預設值。

元件定義的樣式

將 shadow DOM 最實用的功能向下交給「範圍 CSS」

  • 外部頁面的 CSS 選取器不會套用至元件。
  • 內部定義的樣式不會出血。存取範圍僅限主要元素。

shadow DOM 中使用的 CSS 選取器會套用至您的元件。於 這意味著我們可以再次使用通用 ID/類別名稱,而不必擔心 。更簡單的 CSS 選取器是最佳做法 在 Shadow DOM 內部也有助於提升成效。

範例 - 陰影根層級中定義的樣式為本機

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

樣式表的範圍也涵蓋陰影樹狀結構:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

您想知道 <select> 元素如何算繪複選小工具 (而不是 下拉式選單),新增 multiple 屬性時:

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select> 可以根據您新增的屬性,以不同方式設定本身的樣式 宣告網頁元件也可以利用 :host 設定網頁元件樣式 選取器程式碼

範例 - 元件樣式本身

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

:host 的一個難題是,父項頁面中的規則可明確指定 超過元素中定義的 :host 項規則也就是外部風格獲勝。這個 可讓使用者從外部覆寫頂層樣式。其他班次::host 只會在陰影根層級下運作,因此不能在 shadow DOM。

:host(<selector>) 的功能形式可讓您指定主機 (如果主機的話) 符合 <selector>。這個方式是元件 根據使用者互動或狀態或設定內部節點樣式的行為 。

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

根據情境設定樣式

:host-context(<selector>) 會比對元件或其祖系 符合 <selector>。其中一項常見的用途是,根據元件 周遭環境。舉例來說,許多人只要 <html><body>

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

如果是子系,:host-context(.darktheme) 會設定 <fancy-tabs> 樣式 /.darktheme

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() 可用於主題設定,但更好的做法是 使用 CSS 自訂屬性建立樣式掛鉤

設定分散式節點的樣式

::slotted(<compound-selector>) 與分配到 <slot>

假設我們建立了一個名稱徽章元件:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

元件的陰影 DOM 可以設定使用者的 <h2>.title 樣式:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

如果您記得之前的內容,<slot> 不會移動使用者的 Light DOM。時間 節點分配到 <slot> 中,<slot> 會轉譯其 DOM,但 讓節點仍然保持運作發布前套用的樣式 。但在 Light DOM 分散式時,此模型「可以」 需要採用其他樣式 (shadow DOM 所定義的樣式)。

<fancy-tabs> 提供的另一個深入範例:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

在這個範例中,有兩個版位:分頁標題的已命名版位 分頁面板內容的版位使用者選取分頁時,系統會以粗體顯示其選取項目 然後顯示面板做法是選取含有 selected 屬性。添加自訂元素的 JS (未顯示於此處) 屬性。

從外部設定元件樣式

您可以透過多種方式從外部設定元件的樣式。最簡便 是使用標記名稱做為選擇器:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

外部樣式一律會優先於 shadow DOM 中定義的樣式。例如: 如果使用者寫入選取器 fancy-tabs { width: 500px; },系統就會優先顯示 元件的規則::host { width: 650px;}

設定元件樣式只是您到目前為止。但要是您將 是否想設定元件內部樣式?因此,我們需要透過 CSS 自訂 資源。

使用 CSS 自訂屬性建立樣式掛鉤

如果元件作者提供樣式掛鉤,使用者即可調整內部樣式 使用 CSS 自訂屬性。概念上 <slot>。建立「樣式預留位置」供使用者覆寫

範例 - <fancy-tabs> 可讓使用者覆寫背景顏色:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

在其 shadow DOM 中:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

在這種情況下,該元件會使用 black 做為背景值,因為 由使用者提供。否則會預設為 #9E9E9E

進階主題

建立封閉式陰影根 (應避免)

還有另一種名為「closed」的 shadow DOM 變種版本模式。建立 Deployment 時 封閉的陰影樹狀結構,JavaScript 以外的地方將無法存取內部 DOM 元件這與 <video> 等原生元素的運作方式類似。 由於瀏覽器,JavaScript 無法存取 <video> 的 shadow DOM 使用封閉模式陰影根層級進行實作

範例 - 建立封閉式陰影樹狀結構:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

其他 API 也會受到封閉模式的影響:

  • Element.assignedSlot / TextNode.assignedSlot 會傳回 null
  • Event.composedPath() 適用於與陰影內部元素相關聯的事件 傳回 []
,瞭解如何調查及移除這項存取權。

以下摘要說明 不應該使用 建立網頁元件的原因 {mode: 'closed'}:

  1. 人造的安全感,沒有什麼原因可以阻止攻擊者 盜用 Element.prototype.attachShadow

  2. 關閉模式會禁止自訂元素程式碼存取其本身的 shadow DOM。已完成這項操作。而須將一個參考檔案 才能使用 querySelector() 這類的優質產品這完全 破壞關閉模式的原始用途!

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. 關閉模式會讓使用者較不靈活元件。如同你 建構網頁元件後,萬一您忘了加入 而不是每個特徵的分數設定選項。使用者想要的用途。常見的 就是忘記為內部節點加入適當的樣式掛鉤。 在封閉模式下,使用者無法覆寫預設值及調整設定 。存取元件內部資源至關重要, 使用者最終會使用您的元件建立分支、尋找其他元件或 如果工具未應做什麼,便可以提供建議 :(

在 JS 中使用運算單元

shadow DOM API 可用來處理版位和分散式資源的公用程式 節點。這在編寫自訂元素時可派上用場。

運算單元變更事件

當運算單元的分散式節點變更時,會觸發 slotchange 事件。適用對象 例如,使用者從光 DOM 新增/移除子項。

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

如要監控其他類型的 Light DOM 變更類型,請設定 MutationObserver敬上 。

哪些元素在版位中算繪?

有時候,瞭解與版位相關聯的元素會很有幫助。致電 slot.assignedNodes():找出版位正在算繪的元素。 {flatten: true} 選項也會傳回版位的備用內容 (如果沒有節點的話) )。

舉例來說,假設陰影 DOM 如下所示:

<slot><b>fallback content</b></slot>
用量撥打電話結果
<my-component>元件文字</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

元素會指派給哪個版位?

回答反向問題也可以。element.assignedSlot 分享 您指派元素的哪個元件版位。

Shadow DOM 事件模型

事件從 shadow DOM 消失時,其目標會調整以維持 封裝 shadow DOM 提供的也就是說,事件必須重新指定 而不是來自元件的內部元素 shadow DOM。有些事件甚至不會從 shadow DOM 傳播。

「確實」跨越陰影邊界的事件如下:

  • 焦點活動:blurfocusfocusinfocusout
  • 滑鼠事件:clickdblclickmousedownmouseentermousemove
  • 車輪事件:wheel
  • 輸入事件:beforeinputinput
  • 鍵盤事件:keydownkeyup
  • 組成事件:compositionstartcompositionupdatecompositionend
  • DragEvent:dragstartdragdragenddrop 等。

使用提示

如果陰影樹狀結構開啟,呼叫 event.composedPath() 會傳回陣列 事件行經的節點數量

使用自訂事件

無法在陰影樹狀結構中於內部節點上觸發的自訂 DOM 事件 。除非事件是使用 composed: true 標記:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

如果為 composed: false (預設值),消費者將無法監聽事件 請在陰影根層級之外

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

處理焦點

如果您回顧一下 shadow DOM 的事件模型,所觸發的事件 陰影 DOM 內部會經過調整,看起來像是來自代管元素 舉例來說,假設您點選了陰影根目錄中的 <input>

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

focus 事件看起來是來自 <x-focus>,而不是 <input>。 同樣地,document.activeElement 會是 <x-focus>。如果影子根 是使用 mode:'open' 建立 (請參閱關閉模式),您也會 存取獲得重點的內部節點:

document.activeElement.shadowRoot.activeElement // only works with open mode.

如果遊戲中有多個陰影 DOM 層級 (例如 也就是另一個自訂元素),您需要以遞迴方式深入查看陰影根層級,以便 找出 activeElement

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

另一個聚焦選項是 delegatesFocus: true 選項,可展開 陰影樹狀結構中元素的焦點行為:

  • 如果您在 shadow DOM 中按下節點後它不是可聚焦區域, 第一個可聚焦的區域就會變成焦點
  • 當 shadow DOM 中的節點成為焦點時,:focus 會套用到 除了聚焦的元素之外

範例 - delegatesFocus: true 如何變更焦點行為

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

結果

DelegatesFocus: 模式的實際行為。

上方是聚焦於 <x-focus> 時的結果 (使用者點選、分頁進入、 focus() 等),「可點擊的 Shadow DOM 文字」或內部容器的 <input> 已聚焦 (包括 autofocus)。

如果設定 delegatesFocus: false,則會看到以下內容:

DelegatesFocus: false,內部輸入的焦點則聚焦於內部輸入。
delegatesFocus: false 且內部 <input> 已聚焦。
,瞭解如何調查及移除這項存取權。
委派代表:false 和 x 對焦
    會增加焦點 (例如含有 tabindex=&#39;0&#39;)。
delegatesFocus: false<x-focus> 會增加焦點 (例如有 tabindex="0")。
委派代表:false 和「可點擊的陰影 DOM 文字」為
    點擊 (或點擊元素 shadow DOM 中的其他空白區域)。
delegatesFocus: false 和「可點擊的 Shadow DOM 文字」為 點擊 (或點擊元素 shadow DOM 中的其他空白區域)。

提示和秘訣

多年來,我學到一兩件有關編寫網頁元件的知識I 相信這些秘訣對元件編寫和技術的運用很有幫助 對 shadow DOM 偵錯。

使用 CSS 納入

一般來說,網頁元件的版面配置/樣式/繪製作業相當獨立。使用 :host 中適用於 Perf 的 CSS 容器 勝利:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

重設可繼承的樣式

可沿用樣式 (backgroundcolorfontline-height 等) 繼續 才會在 shadow DOM 中沿用也就是說,它們會將陰影 DOM 邊界旋轉 預設值。如要從新的插入畫面開始設定,請使用 all: initial; 進行重設 當跨越陰影邊界時,可繼承的樣式就會成為其初始值。

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

找出網頁使用的所有自訂元素

尋找網頁上使用的自訂元素有時非常實用。如要這麼做, 就需要以遞迴方式掃遍網頁上所有使用元素的 shadow DOM。

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

從 <template> 建立元素

我們可以使用宣告式宣告方式,而不使用 .innerHTML 填入影子根 <template>。範本很適合用來宣告 網頁元件

範例如下: 「自訂元素:建構可重複使用的網頁元件」

歷史和瀏覽器支援

如果您近幾年來持續追蹤網頁元件 Chrome 35+/Opera 為 一些時間。但 Blink 會繼續同時支援兩種版本 讓應用程式從可以最快做出回應的位置 回應使用者要求第 0 版規格提供不同的方法建立影子根目錄 (element.createShadowRoot,而非 v1 的 element.attachShadow)。呼叫 舊版方法會繼續建立使用 v0 語意的陰影根目錄 不會中斷

如果您想瞭解舊版 v0 規格,請參閱 html5rocks 文章: 123: 還有非常棒的比較 shadow DOM v0 和 v1 之間的差異

瀏覽器支援

Shadow DOM v1 於 Chrome 53 版 (狀態) 發布。 Opera 40、Safari 10 和 Firefox 63。邊緣 已開始開發

如要功能偵測陰影 DOM,請檢查 attachShadow 是否存在:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

聚合物

在此之前, shadycss polyfills 可提供第 1 版 而不是每個特徵的分數Shady DOM 模仿 DOM 調整 Shadow DOM 範圍和陰影 polyfill CSS 自訂屬性和樣式設定原生 API 提供的樣式範圍。

安裝 polyfill:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

使用 polyfill:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

詳情請參閱 https://github.com/webcomponents/shadycss#usage

結論

我們首次具備能正確設定 CSS 範圍的 API 基本功能 DOM 限定範圍,且具備真正的組合。與其他網頁元件 API 結合 例如自訂元素,shadow DOM 提供一種方法 讓編寫程式時 無需入侵,或使用 <iframe> 等舊式行李的行李。

不要弄錯了。影子 DOM 真的是很複雜的動物!但這是個獸 值得學習花一點時間瞭解它。瞭解並提出問題!

延伸閱讀

常見問題

我今天可以使用 Shadow DOM v1 嗎?

使用 polyfill 就代表肯定的請參閱瀏覽器支援

shadow DOM 提供哪些安全性功能?

Shadow DOM 並非安全防護功能。這是一種輕量化工具 可用來指定 CSS 範圍 以及隱藏元件中的 DOM 樹狀結構如果您需要真正的資安防護範圍 使用 <iframe>

網頁元件需要使用 shadow DOM 嗎?

不對!您不需要建立使用 shadow DOM 的網頁元件。不過 編寫使用 Shadow DOM 的自訂元素後, 善用 CSS 範圍、DOM 封裝和組合等功能。

開放和封閉的影子根之間有何差異?

請參閱「封閉陰影根層級」一文。