Shadow DOM v1 簡介
- 2019 年 11 月 5 日
- 筆記
什麼是 Shadow DOM
Shadow DOM 是 Web Components 定義的四大標準之一。Shadow DOM 解決了構建網路應用的脆弱性問題。脆弱性是由 HTML、CSS 和 JS 的全局性引起的。
Shadow DOM 為網路開發中的常見問題提供解決方案:
- 隔離 DOM:組件的 DOM 是獨立的(例如,
document.querySelector()
不會返回組件 Shadow DOM 中的節點)。 - 作用域 CSS:Shadow DOM 內部定義的 CSS 在其作用域內。樣式規則不會泄漏,頁面樣式也不會滲入。
- 組合:為組件設計一個聲明性、基於標記的 API。
- 簡化 CSS: 作用域 DOM 意味著您可以使用簡單的 CSS 選擇器,更通用的 id/class 名稱,而無需擔心命名衝突。
- 效率:將應用看成是多個 DOM 塊,而不是一個大的(全局性)頁面。

(圖片來源 —— MDN Shadow DOM)
Shadow DOM vs DOM
HTML 因其易於使用的特點驅動著網路的發展。通過聲明幾個標記,即可在幾秒內編寫一個帶有圖文資訊和結構的頁面。 但是,HTML 自身的功能並不強大。 對於我們人類而言,理解基於文本語言很容易,但是機器需要更多幫助才能理解。 因此,文檔對象模型(DOM) 應運而生。
Shadow DOM 與普通 DOM 相同,但有兩點區別:
1) 創建/使用的方式;
2) 與頁面其他部分有關的行為方式。
這裡以 「創建/使用的方式」 為例:
創建 DOM
const header = document.createElement('header'); const h1 = document.createElement('h1'); h1.textContent = 'Hello world!'; header.appendChild(h1); document.body.appendChild(header);
創建 Shadow DOM
const header = document.createElement('header'); const shadowRoot = header.attachShadow({mode: 'open'}); shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
需要注意的是,並不是所有的元素都可以掛載 Shadow DOM,其主要原因是:
- 瀏覽器已為該元素託管其自身的內部 shadow DOM(比如
textarea
、input
)。 - 讓元素託管 shadow DOM 毫無意義 (比如
img
)。
所以以下方法是行不通的:
document.createElement('input').attachShadow({mode: 'open'});
另外使用 Shadow DOM 時,有以下的注意事項:
- Shadow DOM,一旦創建就無法刪除,它只能用新的替換。
- 要查看瀏覽器如何為 input 或 textarea 等元素實現 shadow DOM,對 Chrome 用戶來說可以按照: DevTools > Settings > Preferences > Elements -> [x] Show user agent shadow DOM路徑啟用對應的選項。
Shadow DOM vs Light DOM
Light DOM
組件用戶編寫的標記,該 DOM 不在組件 shadow DOM 之內,它是元素實際的子項。
<button is="better-button"> <img src="gear.svg" slot="icon"> <span>Settings</span> </button>
Shadow DOM
該 DOM 是由組件的作者編寫。Shadow DOM 對於組件而言是本地的,它定義內部結構、作用域 CSS 並封裝實現詳情。它還可定義如何渲染由組件使用者編寫的標記。
#shadow-root <style>...</style> <slot name="icon"></slot> <span id="wrapper"> <slot>Button</slot> </span>
如何創建 Shadow DOM
<div class="dom"></div> <script> let el = document.querySelector('.dom'); el.attachShadow({ mode: 'open' }); el.shadowRoot.innerHTML = 'Hi I am shadowed!'; // 創建新的元素 let hello = document.createElement('span'); hello.textContent = 'Hi I am shadowed but wrapped in span'; el.shadowRoot.appendChild(hello); </script>
什麼是 Shadow Root
ShadowRoot 是 Shadow DOM 的根,從技術上講,它是一個非元素節點,是一種特殊的文檔片段。你可以通過 ShadowRoot 對象上的 appendChild
、querySelectorAll
等方法去操作整個 Shadow DOM 樹。
對於一個普通的元素,比如 <div>
,你可以通過調用該對象上的 attachShadow
方法來創建一個ShadowRoot,attachShadow
接受一個對象進行初始化,這個對象有一個 mode
屬性,它有兩個取值:'open'
和 'closed'
,這個屬性是在創造 ShadowRoot 的時候需要初始化提供的,並在創建 ShadowRoot 之後成為一個只讀屬性。
那麼 mode: 'open'
和 mode: 'closed'
有什麼區別呢?
let $element = document.createElement("div"); $element.attachShadow({ mode: "open" }); $element.shadowRoot
在調用 attachShadow
創建 ShadowRoot 之後,attachShdow
方法會返回 ShadowRoot 對象實例,你可以通過這個返回的對象去構造整個 Shadow DOM。
當 mode 為 'open'
時,在用於創建 ShadowRoot 的外部普通節點(比如 <div>
)上,會有一個 shadowRoot
屬性,這個屬性也就是創造出來的那個 ShadowRoot,也就是說,你可以通過這個屬性獲取 ShadowRoot,進而對它進行操作。
而當 mode 為 'closed'
時,你將不能再得到這個屬性,這個屬性會被設置為 null
,比如:
let $element = document.createElement("div"); $element.attachShadow({ mode: "closed" }); $element.shadowRoot // null
一般情況下總是使用 open
模式調用 attachShadow 方法,這樣的話可以讓組件作者和用戶都可以根據需要進行相關操作。
如何設定樣式
Shadow DOM 最有用的功能是作用域 CSS:
- 外部頁面中的 CSS 選擇器不應用於組件內部。
- 內部定義的樣式也不會滲出,它們的作用域僅限於宿主元素。
設置宿主元素樣式
<style> :host { display: block; contain: content; } </style>
使用 :host
的一個問題是,父頁面中的規則較之在元素中定義的 :host
規則具有更高的特異性。 也就是說,外部樣式優先。這可讓用戶從外部替換你已定義的樣式。 此外,:host
僅在影子根範圍內起作用,因此無法在 shadow DOM 之外使用。
除了 :host
之外,還支援 :host(<selector>)
的函數形式,它可讓你基於宿主將對用戶互動或狀態的反應行為進行封裝,或對內部節點進行樣式設定。
<style> :host { opacity: 0.4; will-change: opacity; transition: opacity 300ms ease-in-out; } :host(:hover) { opacity: 1; } :host([disabled]) { background: grey; pointer-events: none; opacity: 0.4; } :host(.blue) { color: blue; } :host(.pink) > #tabs { color: pink; </style>
基於情景設定樣式
如果 :host-context(<selector>)
或其任意父級與 <selector>
匹配,它將與組件匹配。 一個常見用途是根據組件的環境進行主題化。 例如,很多人都通過將 class 應用到 <html>
或 <body>
進行主題化:
<body class="darktheme"> <fancy-tabs> ... </fancy-tabs> </body>
如果 :host-context(.darktheme)
為 .darktheme
的子級,它將對 <fancy-tabs>
進行樣式化:
:host-context(.darktheme) { color: white; background: black; }
為分散式節點設定樣式
比如說我們已創建了一個 name badge 組件:
<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; } /* 以下方式不生效,因為::slotted()只支援頂層元素 ::slotted(.company), ::slotted(.title .company) { text-transform: uppercase; } */ </style> <slot></slot>
從外部為組件設定樣式
有幾種方法可從外部為組件設定樣式:最簡單的方法是使用標籤名稱作為選擇器:
fancy-tabs { width: 500px; color: red; } fancy-tabs:hover { box-shadow: 0 3px 3px #ccc; }
外部樣式總是優先於在 shadow DOM 中定義的樣式。例如,如果用戶編寫選擇器 fancy-tabs { width: 500px; }
,它將優先於組件的規則::host { width: 650px;}
。
使用 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
。
瀏覽器支援

(圖片來源 —— https://caniuse.com/#feat=shadowdomv1)
由上圖可知 Chrome 53、Opera 40 和 Safari 10 以上的版本是支援 shadow DOM v1 標準。 Edge 也在考慮中,並具有較高的優先順序。
你可以通過以下方法來檢測當前瀏覽器是否支援 shadow DOM v1 標準:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
此外對於還不支援 shadow DOM v1 標準的瀏覽器來說,你也可以引入 shadydom 和 shadycss polyfill 來模擬 v1 的標準。Shady DOM 可以模擬 Shadow DOM 的 DOM 作用域,而 shadycss polyfill 則可以模擬原生 API 提供的 CSS 自定義屬性和樣式作用域。具體的使用方式,感興趣的同學,請參考相應的開發文檔,這裡不再進一步說明。
參考資源
- 深度介紹:你聽說過原生 HTML 組件嗎?
- Shadow DOM v1:獨立的網路組件