翻譯:瘋狂的技術宅
https://blog.logrocket.com/un...
本文首發微信公衆號:前端先鋒
歡迎關注,天天都給你推送新鮮的前端技術文章javascript
shadow DOM不是超級英雄電影中的惡棍,也不是DOM的黑暗面。 shadow DOM只是一種解決文檔對象模型(或簡稱DOM)中缺乏的樹封裝方法。css
網頁一般使用來自外部源的數據和小部件,若是它們沒有封裝,那麼樣式可能會影響HTML中沒必要要的部分,迫使開發人員使用特定的選擇器和!important
規則來避免樣式衝突。html
儘管如此,在編寫大型程序時,這些努力彷佛並非那麼有效,而且大量的時間被浪費在防止CSS和JavaScript的衝突上。 Shadow DOM API旨在經過提供封裝DOM樹的機制來解決這些問題。前端
Shadow DOM是用於建立Web組件的主要技術之一,另外兩個是自定義元素和HTML模板。 Web 組件的規範最初是由Google提出的,用於簡化Web小部件的開發。java
雖然這三種技術旨在協同工做,不過你能夠自由地分別使用每種技術。本教程的範圍僅限於shadow DOM。node
在深刻研究如何建立shadow DOM以前,瞭解DOM是什麼很是重要。 W3C文檔對象模型(DOM)提供了一個平臺和語言無關的應用程序編程接口(API),用於表示和操做存儲在HTML和XML文檔中的信息。git
經過使用DOM,程序員能夠訪問、添加、刪除或更改元素和內容。 DOM將網頁視爲樹結構,每一個分支以節點結束,每一個節點包含一個對象,可使用JavaScript等腳本語言對其進行修改。請考慮如下HTML文檔:程序員
<html> <head> <title>Sample document</title> </head> <body> <h1>Heading</h1> <a href="https://example.com">Link</a> </body> </html>
此HTML的DOM表示以下:github
此圖中全部的框都是節點。web
用於描述DOM部分的術語相似於現實世界中的家譜樹:
節點的類型取決於它所表明的HTML元素的類型。 HTML標記被稱爲元素節點。嵌套標籤造成一個元素樹。元素中的文本稱爲文本節點。文本節點可能沒有子節點,你能夠把它想象成是一棵樹的葉子。
爲了訪問樹,DOM提供了一組方法,程序員能夠用這些方法修改文檔的內容和結構。例如當你寫下document.createElement('p');
時,就在使用DOM提供的方法。沒有DOM,JavaScript就沒法理解HTML和XML文檔的結構。
下面的JavaScript代碼顯示瞭如何使用DOM方法建立兩個HTML元素,將一個嵌套在另外一個內部並設置文本內容,最後把它們附加到文檔正文:
const section = document.createElement('section'); const p = document.createElement('p'); p.textContent = 'Hello!'; section.appendChild(p); document.body.appendChild(section);
這是運行這段JavaScript代碼後生成的DOM結構:
<body> <section> <p>Hello!</p> </section> </body>
封裝是面向對象編程的基本特性,它使程序員可以限制對某些對象組件的未受權訪問。
在此定義下,對象以公共訪問方法的形式提供接口做爲與其數據交互的方式。這樣對象的內部表示不能直接被對象的外部訪問。
Shadow DOM將此概念引入HTML。它容許你將隱藏的,分離的DOM連接到元素,這意味着你可使用HTML和CSS的本地範圍。如今能夠用更通用的CSS選擇器而沒必要擔憂命名衝突,而且樣式再也不泄漏或被應用於不恰當的元素。
實際上,Shadow DOM API正是庫和小部件開發人員將HTML結構、樣式和行爲與代碼的其餘部分分開所需的東西。
Shadow root 是 shadow 樹中最頂層的節點,是在建立 shadow DOM 時被附加到常規DOM節點的內容。具備與之關聯的shadow root的節點稱爲shadow host。
你能夠像使用普通DOM同樣將元素附加到shadow root。連接到shadow root的節點造成 shadow 樹。經過圖表應該可以表達的更清楚:
術語light DOM一般用於區分正常DOM和shadow DOM。shadow DOM和light DOM被並稱爲邏輯DOM。light DOM與shadow DOM分離的點被稱爲陰影邊界。 DOM查詢和CSS規則不能到達陰影邊界的另外一側,從而建立封裝。
要建立shadow DOM,須要用Element.attachShadow()
方法將shadow root附加到元素:
var shadowroot = element.attachShadow(shadowRootInit);
來看一個簡單的例子:
<div id="host"><p>Default text</p></div> <script> const elem = document.querySelector('#host'); // attach a shadow root to #host const shadowRoot = elem.attachShadow({mode: 'open'}); // create a <p> element const p = document.createElement('p'); // add <p> to the shadow DOM shadowRoot.appendChild(p); // add text to <p> p.textContent = 'Hello!'; </script>
此代碼將一個shadow DOM樹附加到div
元素,其id
是host
。這個樹與div
的實際子元素是分開的,添加到它之上的任何東西都將是託管元素的本地元素。
Chrome DevTools中的 Shadow root。
注意#host
中的現有元素是如何被shadow root替換的。不支持shadow DOM的瀏覽器將使用默認內容。
如今,在將CSS添加到主文檔時,樣式規則不會影響shadow DOM:
<div><p>Light DOM</p></div> <div id="host"></div> <script> const elem = document.querySelector('#host'); // attach a shadow root to #host const shadowRoot = elem.attachShadow({mode: 'open'}); // set the HTML contained within the shadow root shadowRoot.innerHTML = '<p>Shadow DOM</p>'; </script> <style> p {color: red} </style>
在light DOM中定義的樣式不能越過shadow邊界。所以,只有light DOM中的段落纔會變爲紅色。
相反,你添加到shadow DOM的CSS對於hosting元素來講是本地的,不會影響DOM中的其餘元素:
<div><p>Light DOM</p></div> <div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <p>Shadow DOM</p> <style>p {color: red}</style>`; </script>
你還能夠將樣式規則放在外部樣式表中,以下所示:
shadowRoot.innerHTML = ` <p>Shadow DOM</p> <link rel="stylesheet" href="style.css">`;
要獲取 shadowRoot
附加到的元素的引用,使用host
屬性:
<div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); console.log(shadowRoot.host); // => <div id="host"></div> </script>
要執行相反操做並獲取對元素託管的shadow root的引用,能夠用元素的shadowRoot
屬性:
<div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); console.log(elem.shadowRoot); // => #shadow-root (open) </script>
當調用Element.attachShadow()
方法來附加shadow root時,必須經過傳遞一個對象做爲參數來指定shadow DOM樹的封裝模式,不然將會拋出一個TypeError
。該對象必須具備mode
屬性,其值爲 open
或 closed
。
打開的shadow root容許你使用host元素的shadowRoot
屬性從root外部訪問shadow root的元素,以下例所示:
<div><p>Light DOM</p></div> <div id="host"></div> <script> const elem = document.querySelector('#host'); // attach an open shadow root to #host const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = `<p>Shadow DOM</p>`; // Nodes of an open shadow DOM are accessible // from outside the shadow root elem.shadowRoot.querySelector('p').innerText = 'Changed from outside the shadow root'; elem.shadowRoot.querySelector('p').style.color = 'red'; </script>
可是若是mode屬性的值爲「closed」,則嘗試從root外部用JavaScript訪問shadow root的元素時會拋出一個TypeError
:
<div><p>Light DOM</p></div> <div id="host"></div> <script> const elem = document.querySelector('#host'); // attach a closed shadow root to #host const shadowRoot = elem.attachShadow({mode: 'closed'}); shadowRoot.innerHTML = `<p>Shadow DOM</p>`; elem.shadowRoot.querySelector('p').innerText = 'Now nodes cannot be accessed from outside'; // => TypeError: Cannot read property 'querySelector' of null </script>
當mode設置爲closed
時,shadowRoot
屬性返回null
。由於null
值沒有任何屬性或方法,因此在它上面調用querySelector()
會致使TypeError
。瀏覽器一般用關閉的 shadow roo 來使某些元素的實現內部不可訪問,並且不可從JavaScript更改。
要肯定shadow DOM是處於open仍是closed模式,你能夠參考shadow root的mode
屬性:
<div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'closed'}); console.log(shadowRoot.mode); // => closed </script>
從表面上看,對於不但願公開其組件的shadow root 的Web組件做者來講,封閉的shadow DOM看起來很是方便,然而在實踐中繞過封閉的shadow DOM並不難。一般徹底隱藏shadow DOM所需的工做量超過了它的價值。
只有一組有限的元素能夠託管shadow DOM。下表列出了支持的元素:
+----------------+----------------+----------------+ | article | aside | blockquote | +----------------+----------------+----------------+ | body | div | footer | +----------------+----------------+----------------+ | h1 | h2 | h3 | +----------------+----------------+----------------+ | h4 | h5 | h6 | +----------------+----------------+----------------+ | header | main | nav | +----------------+----------------+----------------+ | p | section | span | +----------------+----------------+----------------+
嘗試將shadow DOM樹附加到其餘元素將會致使「DOMException」錯誤。例如:
document.createElement('img').attachShadow({mode: 'open'}); // => DOMException
用<img>
元素做爲shadow host是不合理的,所以這段代碼拋出錯誤並不奇怪。你可能會收到DOMException
錯誤的另外一個緣由是瀏覽器已經用該元素託管了shadow DOM。
Shadow DOM已存在很長一段時間了,瀏覽器一直用它來隱藏元素的內部結構,好比<input>
,<textarea>
和<video>
。
當你在HTML中使用<video>
元素時,瀏覽器會自動將shadow DOM附加到包含默認瀏覽器控件的元素。但DOM中惟一可見的是<video>
元素自己:
要在Chrome中顯示此類元素的shadow root,請打開Chrome DevTools設置(按F1),而後在「elements」部分下方選中「Show user agent shadow DOM」:
選中「Show user agent shadow DOM」選項後,shadow root節點及其子節點將變爲可見。如下是啓用此選項後相同代碼的顯示方式:
Custom Elements API 建立的自定義元素能夠像其餘元素同樣託管shadow DOM。請看如下示例:
<my-element></my-element> <script> class MyElement extends HTMLElement { constructor() { // must be called before the this keyword super(); // attach a shadow root to <my-element> const shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <style>p {color: red}</style> <p>Hello</p>`; } } // register a custom element on the page customElements.define('my-element', MyElement); </script>
此代碼了建立一個託管shadow DOM的自定義元素。它調用了customElements.define()
方法,元素名稱做爲第一個參數,類對象做爲第二個參數。該類擴展了HTMLElement
並定義了元素的行爲。
在構造函數中,super()
用於創建原型鏈,而且把Shadow root附加到自定義元素。當你在頁面上使用<my-element>
時,它會建立本身的shadow DOM:
請記住,有效的自定義元素不能是單個單詞,而且名稱中必須包含連字符( - )。例如,myelement不能用做自定義元素的名稱,並會拋出 DOMException
錯誤。
一般,要設置host元素的樣式,你須要將CSS添加到light DOM,由於這是host元素所在的位置。可是若是你須要在shadow DOM中設置host元素的樣式呢?
這就是host()
僞類函數的用武之地。這個選擇器容許你從shadow root中的任何地方訪問shadow host。這是一個例子:
<div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <p>Shadow DOM</p> <style> :host { display: inline-block; border: solid 3px #ccc; padding: 0 15px; } </style>`; </script>
值得注意的是:host
僅在shadow root中有效。還要記住,在shadow root以外定義的樣式規則比:host
中定義的規則具備更高的特殊性。
例如,#host { font-size: 16px; }
的優先級高於 shadow DOM的 :host { font-size: 20px; }
。實際上這頗有用,這容許你爲組件定義默認樣式,並讓組件的用戶覆蓋你的樣式。惟一的例外是!important
規則,它在shadow DOM中具備特殊性。
你還能夠將選擇器做爲參數傳遞給:host()
,這容許你僅在host與指定選擇器匹配時纔會定位host。換句話說,它容許你定位同一host的不一樣狀態:
<style> :host(:focus) { /* style host only if it has received focus */ } :host(.blue) { /* style host only if has a blue class */ } :host([disabled]) { /* style host only if it's disabled */ } </style>
要選擇特定祖先內部的shadow root host ,能夠用:host-context()
僞類函數。例如:
:host-context(.main) { font-weight: bold; }
只有當它是.main
的後代時,此CSS代碼纔會選擇shadow host :
<body class="main"> <div id="host"> </div> </body>
:host-context()
對主題特別有用,由於它容許做者根據組件使用的上下文對組件進行樣式設置。
shadow DOM的一個有趣地方是它可以建立「樣式佔位符」並容許用戶填充它們。這能夠經過使用CSS自定義屬性來完成。咱們來看一個簡單的例子:
<div id="host"></div> <style> #host {--size: 20px;} </style> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <p>Shadow DOM</p> <style>p {font-size: var(--size, 16px);}</style>`; </script>
這個shadow DOM容許用戶覆蓋其段落的字體大小。使用自定義屬性表示法( — size: 20px
)設置該值,而且shadow DOM用var()
函數(font-size: var( — size, 16px)
)檢索該值。在概念方面,這相似於<slot>
元素的工做方式。
shadow DOM容許你建立獨立的DOM元素,而不會從外部看到選擇器可見性,但這並不意味着繼承的屬性不會經過shadow邊界。
某些屬性(如color
,background
和font-family
)會傳遞shadow邊界並應用於shadow樹。所以,與iframe相比,shadow DOM不是一個很是強大的障礙。
<style> div { font-size: 25px; text-transform: uppercase; color: red; } </style> <div><p>Light DOM</p></div> <div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = `<p>Shadow DOM</p>`; </script>
解決方法很簡單:經過聲明all: initial
將可繼承樣式重置爲其初始值,以下所示:
<style> div { font-size: 25px; text-transform: uppercase; color: red; } </style> <div><p>Light DOM</p></div> <div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <p>Shadow DOM</p> <style> :host p { all: initial; } </style>`; </script>
在此例中,元素被強制回到初始狀態,所以穿過shadow邊界的樣式不起做用。
在shadow DOM內觸發的事件能夠穿過shadow邊界並冒泡到light DOM;可是,Event.target
的值會自動更改,所以它看起來好像該事件源自其包含的shadow樹而不是實際元素的host元素。
此更改稱爲事件重定向,其背後的緣由是保留shadow DOM封裝。請參考如下示例:
<div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <ul> <li>One</li> <li>Two</li> <li>Three</li> <ul> `; document.addEventListener('click', (event) => { console.log(event.target); }, false); </script>
當你單擊shadow DOM中的任何位置時,這段代碼會將 <div id =「host」> ... </div>
記錄到控制檯,所以偵聽器沒法看到調度該事件的實際元素。
可是在shadow DOM中不會發生重定目標,你能夠輕鬆找到與事件關聯的實際元素:
<div id="host"></div> <script> const elem = document.querySelector('#host'); const shadowRoot = elem.attachShadow({mode: 'open'}); shadowRoot.innerHTML = ` <ul> <li>One</li> <li>Two</li> <li>Three</li> </ul>`; shadowRoot.querySelector('ul').addEventListener('click', (event) => { console.log(event.target); }, false); </script>
請注意,並不是全部事件都會從shadow DOM傳播出去。那些作的是從新定位,但其餘只是被忽略了。若是你使用自定義事件的話,則須要使用composed:true
標誌,不然事件不會從shadow邊界冒出來。
Shadow DOM規範的原始版本在 Chrome 25 中實現,當時稱爲Shadow DOM v0。該規範的新版本改進了Shadow DOM API的許多方面。
例如,一個元素不能再承載多個shadow DOM,而某些元素根本不能託管shadow DOM。違反這些規則會致使錯誤。
此外,Shadow DOM v1提供了一組新功能,例如打開 shadow 模式、後備內容等。你能夠找到由規範做者之一編寫的 v0 和 v1 之間的全面比較(https://hayato.io/2016/shadow...)。能夠在W3C找到Shadow DOM v1的完整描述。
在撰寫本文時,Firefox和Chrome已經徹底支持Shadow DOM v1。不幸的是,Edge還沒有實現v1,Safari 只是部分支持。在 Can I use…上提供了支持的瀏覽器的最新列表。
要在不支持Shadow DOM v1的瀏覽器上實現shadow DOM,能夠用shadydom和shadycss polyfills。
DOM開發中缺少封裝一直是個問題。 Shadow DOM API爲咱們提供了劃分DOM範圍的能力,從而爲這個問題提供了一個優雅的解決方案。
如今,樣式衝突再也不是一個使人擔心的問題,選擇器也不會失控。 shadow DOM改變了小部件開發的遊戲規則,可以建立從頁面其他部分封裝的小部件,而且不受其餘樣式表和腳本的影響,這是一個巨大的優點。
如前所述,Web 組件由三個主要技術組成,而shadow DOM是其中的關鍵部分。但願在閱讀本文以後,你將更容易理解這三種技術是如何協同構建Web組件的。