Shadow DOM 可讓網頁開發人員為網頁元件建立獨立的 DOM 和 CSS
摘要
Shadow DOM 免除建構網頁應用程式的麻煩。優點是來自 HTML、CSS 和 JS 的全域性質。多年來,我們打造了大量數量tools來規避問題。舉例來說,使用新的 HTML ID/類別時,系統不會判斷是否與網頁所用的現有名稱發生衝突。難以察覺的錯誤:CSS 特異問題已成為一大問題 (!important
所有事情!),樣式選擇器會變得無法控制,而且效能可能會受到影響。清單繼續。
Shadow DOM 修正 CSS 和 DOM。為網路平台導入範圍樣式。如果沒有工具或命名慣例,您可以在基本 JavaScript 中組合 CSS 與標記、隱藏實作詳細資料,並撰寫獨立的元件。
簡介
Shadow DOM 是三種 Web 元件標準之一:HTML 範本、Shadow DOM 和自訂元素。HTML 匯入過去是用於清單,但現在視為已淘汰。
您不必編寫使用 shadow DOM 的網頁元件。但如此一來,您就可以利用這項服務的優勢 (CSS 範圍、DO 封裝、組合),並建構可重複使用的自訂元素,這些自訂元素不僅更有彈性、高度設定彈性,而且可極度重複使用。如果自訂元素是建立新 HTML 的方式 (使用 JS API),陰影 DOM 是您提供其 HTML 和 CSS 的方式。這兩個 API 可合併成一個元件,其中含有獨立的 HTML、CSS 和 JavaScript。
Shadow DOM 是用於建構元件式應用程式的工具。因此,它能提供網站開發常見問題的解決方案:
- 獨立 DOM:元件的 DOM 是獨立性 (舉例來說,
document.querySelector()
不會傳回元件 shadow DOM 中的節點)。 - 限定範圍的 CSS:在 shadow DOM 中定義的 CSS 限定範圍。樣式規則不會外洩,而且頁面樣式不會流出。
- 組合:為元件設計宣告式標記式 API。
- 簡化 CSS - Scoped DOM 代表您可以使用簡單的 CSS 選取器、更通用的 ID/類別名稱,而且不必擔心命名衝突。
- 效率提升 - 將應用程式看成一個影片的 DOM,而不是一個大型 (全球) 頁面。
fancy-tabs
示範
在本文中,我將是指示範元件 (<fancy-tabs>
),並參照其中的程式碼片段。如果瀏覽器支援 API,您應該在下方看到實際示範。如果沒有,請查看 GitHub 上的完整原始碼。
什麼是 shadow DOM?
DOM 背景
HTML 是網路的驅動力,因為使用起來輕而易舉。只要宣告一些標記,就能在幾秒內編寫同時具有呈現和結構的頁面。不過,HTML 本身並非非常實用。人類很容易懂得文字型語言 但機器需要更多輸入文件物件模型或 DOM
瀏覽器載入網頁時,會呈現許多有趣的資訊。其中一個功能是將作者的 HTML 轉換為即時文件。基本上,為了瞭解網頁的結構,瀏覽器會將 HTML (文字的靜態字串) 剖析為資料模型 (物件/節點)。瀏覽器會建立這些節點的樹狀結構 (也就是 DOM),以保留 HTML 的階層。DOM 的特色在於提供網頁的即時表示法。與我們編寫的靜態 HTML 不同,瀏覽器產生的節點包含屬性和方法,最棒的是,也可以透過程式來操控!這就是我們直接使用 JavaScript 建立 DOM 元素的原因:
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
陰影根是附加至「主機」元素的文件片段。附加陰影根的動作是元素取得 shadow 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 API。這是網路我們是我們的選擇。
規格定義了無法代管陰影樹狀結構的元素清單。清單中含有某個元素的可能原因如下:
- 瀏覽器已自行代管元素的內部陰影 DOM (
<textarea>
、<input>
)。 - 元素代管 shadow DOM (
<img>
)。
舉例來說,這樣無法運作:
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
為自訂元素建立 shadow DOM
Shadow 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>
。
組合和運算單元
組合是 shadow DOM 最不瞭解的特徵之一,但可說是最重要的功能。
在網頁開發的世界中,組成是建構應用程式 (透過 HTML 宣告) 的方式。不同的建構模塊 (<div>
、<header>
、<form>
、<input>
) 會一起形成應用程式。部分代碼甚至可以互相搭配運作組合是 <select>
、<details>
、<form>
和 <video>
等原生元素如此彈性的原因。每個標記都接受特定 HTML 做為子項,並搭配特殊功能。舉例來說,<select>
知道如何在下拉式選單和多選小工具中算繪 <option>
和 <optgroup>
。<details>
元素會將 <summary>
轉譯為可展開的箭頭。即使 <video>
知道如何處理特定子項:<source>
元素不會轉譯,但會影響影片的行為。什麼魔法!
術語:Light DOM 與 shadow DOM
陰影 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>
Shadow 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> 元素
Shadow 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 選取器不適用於元件。
- 內部定義的樣式不會外流。因此只能在主要元素中使用。
陰影 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>
新增 multiple
屬性時,不妨試想 <select>
元素會如何轉譯複選小工具 (而非下拉式選單):
<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>
當 <fancy-tabs>
是 .darktheme
的子系時,:host-context(.darktheme)
會設定 <fancy-tabs>
的樣式:
: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>
元件的 shadow 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
。
進階主題
建立封閉式陰影根 (應避免)
另外還有一種稱為「關閉」模式的 shadow DOM。在建立封閉的陰影樹狀結構時,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()
:與陰影 DOM 內元素相關聯的事件傳回 []
我的摘要說明您不應使用 {mode: 'closed'}
建立網頁元件的原因:
以人為本的原則,提供安全性。沒有什麼可以阻止攻擊者綁架
Element.prototype.attachShadow
。封閉模式會禁止自訂元素程式碼存取自己的 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'); } ... });
關閉模式會降低使用者的元件彈性。當您建構網頁元件時,有時可能會忘記新增功能。一個設定選項。使用者想要的用途。常見的例子是忘記為內部節點加入適當的樣式掛鉤。在關閉模式下,使用者無法覆寫預設值和微調樣式。能存取元件的內部結構將非常實用。最後,如果使用者無法執行所需作業,最終將會為元件建立分支、尋找其他元件,或是自行建立元件 :(
在 JS 中使用版位
shadow DOM API 提供適用於運算單元和分散式節點的公用程式。編寫自訂元素時特別實用。
Slotchange 活動
當運算單元的分散式節點變更時,就會觸發 slotchange
事件。例如,如果使用者從 light DOM 中新增/移除子項。
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('light dom children changed!');
});
如要監控 Light DOM 的其他類型變更,您可以在元素的建構函式中設定 MutationObserver
。
版位中目前顯示哪些元素?
有時候,瞭解哪些元素與版位相關聯。呼叫 slot.assignedNodes()
即可找出版位算繪的元素。{flatten: true}
選項也會傳回運算單元的備用內容 (在未分配任何節點的情況下)。
舉例來說,假設 shadow DOM 如下所示:
<slot><b>fallback content</b></slot>
使用方式 | 撥打電話 | 結果 |
---|---|---|
<my-component>元件文字</my-component> | slot.assignedNodes(); |
[component text] |
<my-component></my-component> | slot.assignedNodes(); |
[] |
<my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
系統會將元素指派給哪個版位?
你也可以回答反向問題。element.assignedSlot
可讓您瞭解要將元素指派給哪個元件版位。
Shadow DOM 事件模型
當事件泡泡從陰影 DOM 上方顯示時,目標會經過調整,以維持 shadow DOM 提供的封裝。也就是說,事件會重新鎖定,外觀是來自元件,而不是 shadow DOM 中的內部元素。部分事件甚至不會傳播 shadow DOM。
「會」跨越陰影邊界的事件如下:
- 焦點事件:
blur
、focus
、focusin
、focusout
- 滑鼠事件:
click
、dblclick
、mousedown
、mouseenter
、mousemove
等等。 - 輪盤事件:
wheel
- 輸入事件:
beforeinput
、input
- 鍵盤事件:
keydown
、keyup
- 組合事件:
compositionstart
、compositionupdate
、compositionend
- DragEvent:
dragstart
、drag
、dragend
、drop
等
使用提示
如果陰影樹是開啟的,呼叫 event.composedPath()
將傳回事件經過的節點陣列。
使用自訂事件
除非使用 composed: true
旗標建立事件,否則在陰影樹狀結構中於內部節點上觸發的自訂 DOM 事件,不會顯出陰影邊界之外:
// 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
選項,它會展開陰影樹狀結構中元素的焦點行為:
- 如果點選陰影 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:True 行為。](https://web.dev/static/articles/shadowdom-v1/image/delegatesfocus-true-beha-2829e235044ab.png?authuser=7&hl=zh-tw)
上方是聚焦 <x-focus>
時的結果 (使用者點擊、按 Tab 鍵進入、focus()
等)。已點選「可點擊的陰影 DOM 文字」,或是內部 <input>
聚焦 (包括 autofocus
)。
如果設定 delegatesFocus: false
,則會看到以下內容:
![DelegatesFocus:false 且聚焦於內部輸入。](https://web.dev/static/articles/shadowdom-v1/image/delegatesfocus-false-th-a0624bcb139e9.png?authuser=7&hl=zh-tw)
delegatesFocus: false
和內部 <input>
聚焦。
![DelegatesFocus:false 和 x 對焦可取得焦點 (例如含有 tabindex='0')。](https://web.dev/static/articles/shadowdom-v1/image/delegatesfocus-false-x-ad7729291b314.png?authuser=7&hl=zh-tw)
delegatesFocus: false
和 <x-focus>
獲得焦點 (例如有 tabindex="0"
)。
![委派聚焦:按一下「false」,以及「可點擊的陰影 DOM 文字」(或是點選元素 shadow DOM 中的其他空白區域)。](https://web.dev/static/articles/shadowdom-v1/image/delegatesfocus-false-c-29c711da9b14f.png?authuser=7&hl=zh-tw)
delegatesFocus: false
和「可點擊的陰影 DOM 文字」
或元素 shadow DOM 中的其他空白區域。
秘訣與指南
多年來,我學到如何編寫網頁元件,我認為這些訣竅能幫助您編寫元件及對 shadow DOM 進行偵錯。
使用 CSS 限制
一般來說,網頁元件的版面配置/樣式/繪製方式相當獨立。在 :host
中使用 CSS 納入來取得 Perf 勝出:
<style>
:host {
display: block;
contain: content; /* Boom. CSS containment FTW. */
}
</style>
重設可沿用的樣式
可繼承的樣式 (background
、color
、font
、line-height
等) 會繼續在 shadow DOM 中沿用。也就是說,這些類型預設會略過 shadow 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 已經推出舊版 shadow DOM。一段時間內,Blink 會繼續同時支援兩個版本。v0 規格提供了不同的方法建立陰影根 (element.createShadowRoot
而非 v1 的 element.attachShadow
)。呼叫較舊版本的方法會繼續建立具有 v0 語意的陰影根,讓現有的 v0 程式碼不會損毀。
如果您想瞭解舊版 v0 規格,請參閱 html5rocks 文章:1、2、3。另外,shadow DOM v0 與 v1 的差異也有相當程度的比較。
瀏覽器支援
Shadow DOM v1 隨附於 Chrome 53 (狀態)、Opera 40、Safari 10 和 Firefox 63 中。Edge 已開始開發。
如要進行功能偵測陰影 DOM,請檢查 attachShadow
是否存在:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
聚酯纖維
在瀏覽器廣泛支援之前,shadydom 和 shadycss polyfill 會提供 v1 功能。Shady DOM 模仿 Shadow DOM 和 shadycss polyfills CSS 自訂屬性的 DOM 範圍,以及原生 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,瞭解如何修飾/範圍您的樣式。
結論
我們第一次採用 API 基本功能,可正確設定 CSS 範圍、DOM 範圍設定,並具備真正的組成元素。並結合其他網頁元件 API (例如自訂元素) 後,shadow DOM 可讓你撰寫真正封裝的元件,無需遭到駭客入侵,或使用 <iframe>
等較舊的行李。
別犯我,陰影 DOM 一定是複雜的怪獸!但這實在值得學習不妨花點時間看看。學以致用,並提問!
其他資訊
- Shadow DOM 第 1 版和 v0 之間的差異
- WebKit 網誌的「隆重推出 Slot-Based Shadow DOM API」一文。
- Philip Walton 所著的網頁元件和模組 CSS 的未來發展
- 參閱 Google 的 WebFundamentals 提供的「自訂元素:建構可重複使用的網頁元件」。
- Shadow DOM v1 規格
- 自訂元素 v1 規格
常見問題
我今天可以使用 Shadow DOM 第 1 版嗎?
使用 polyfill請參閱「瀏覽器支援」。
shadow DOM 提供哪些安全性功能?
Shadow DOM 並非安全性功能。這項輕量工具可用來限定 CSS 範圍,以及在元件中隱藏 DOM 樹狀結構。如要加入真正的安全邊界,請使用 <iframe>
。
網頁元件是否需要使用 shadow DOM?
不對!您不必建立使用 shadow DOM 的網頁元件。不過,編寫使用 Shadow DOM 的自訂元素後,就能使用 CSS 範圍、DOM 封裝和組合等功能。
開放式和封閉式陰影根之間有什麼差別?
請參閱「關閉的陰影根」。