理解Shadow DOM(一)

龍恩0707發表於2019-03-17

1. 什麼是Shadow DOM?

Shadow DOM 如果按照英文翻譯的話可以理解為 影子DOM, 何為影子DOM呢?可以理解為一般情況下使用肉眼看不到的DOM結構,那如果一般情況下看不到的話,那也就是說我們無法直接控制操縱的DOM結構。
Shadow DOM 它是HTML的一個規範,它允許在文件(document)渲染時插入一顆DOM元素子樹,但是這個子樹不在主DOM樹中。
它允許瀏覽器開發者封裝自己的HTML標籤、css樣式和特定的javascript程式碼、同時開發人員也可以建立類似 <input>、<video>、<audio>等、這樣的自定義的一級標籤。建立這些標籤內容相關的API,可以被叫做 Web Component。

如上基本解釋,讓我們很難理解,因此我們可以先看下如下一個 input標籤的demo吧。

html程式碼如下:

<!DOCTYPE html>
<html>
  <head>
    <title>Shadow DOM</title>
    <style>
      #app {margin: 100px;}
      input::-webkit-input-placeholder {
        color: red;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <input type="number" placeholder="請輸入內容" />
    </div>
  </body>
</html>

然後我們在瀏覽器檢視如下所示:

如上程式碼可以看到,我們使用 input::-webkit-input-placeholder{color: red;} 偽類後,input中的placeholder字型顏色發生改變了,但是我們的input元素的結構並沒有看到偽類相關的html結構。

為了能看到基本結構,我們只需要在chrome瀏覽器中,開啟開發者工具,點選右上角的 "Settings"按鈕,勾選 "Show use agent shadow DOM". 後 如下圖所示:

然後我們再來看下input元素的基本程式碼結構如下看到:

如上截圖所示,我們可以看到 "請輸入內容" 中的div元素上有一個屬性為 pseudo="-webkit-input-placeholder", 因此我們使用 input元素的偽類 input::-webkit-input-placeholder{} 這樣就可以控制元素的樣式了。

2. ShadowDOM 存在的意義?

首先我們來看下Shadow-dom 基本的結構如下:

Document: 是document文件物件。
shadow-host: Shadow DOM的容器元素,即:它是Shadow DOM的一個宿主元素。比如:<input />、<audio>、<video> 標籤;就是shadow-dom的宿主元素。
shadow-root: Shadow DOM的根節點。通過createShadowRoot返回的文件片段被稱為 shadow-root, 它和它的後代元素,都會對使用者隱藏。但是它會在瀏覽器中被渲染的,也就是說它是存在的,但是一般情況下在瀏覽器中是不顯示出來的。在chrome瀏覽器中,如上我們可以看到的。
contents: Shadow DOM包含的子節點樹結構。它包含 <input />、<audio>、<video>等標籤中各子元件的DOM的具體實現。

那麼ShadowDOM存在的意義是?

我們都知道像React或Vue這樣的都有元件的概念,比如element-ui等這樣的vue元件。但是我們常用的input、audio、video、等這些元素,其實它也是以元件的形式存在的,即:HTML Web Component. 即這些都有 Shadow DOM。

因此我們可以認為像 input, audio, video等這些元素也是以元件的形式存在的。那麼這些元件內部是由一些HTML標籤組成的。
這些元素組成了DOM樹的子樹。但是當我們使用input,audio,或video等這些元素元件的時候,都會知道該子樹的結構,當我們訪問網頁DOM結構的時候,這些子樹都會暴露出來,當我們使用css樣式去改變DOM的樣式的時候,如果DOM的類名和該子樹的類名相同的話,會和子樹的樣式產生衝突,並且我們使用控制元件的時候,我們並不關心控制元件的內部結構,只關心控制元件本身,因此我們需要將控制元件的內部資訊封裝起來。因此 W3C提出了 ShadowDOM的概念,ShadowDOM可以使一些DOM節點在特定範圍內可見,在網頁中是不可見的。但是在頁面渲染的時候也會渲染該ShadowDOM。
也可以看這篇文章介紹

ShadowDOM在各個瀏覽器的支援程度呢?

檢視Can I Use(https://caniuse.com/#search=shadowDOM) 可以看到它在瀏覽器下的支援程度,如下所示:

3. 如何控制 shadow-dom?

既然W3C提出了ShadowDOM的概念,並且在網頁中一般是不可見的,那麼我們是否可以控制該 shadow-dom呢?
下面我們以 <video>標籤來講解吧,比如如下HTML程式碼:

<!DOCTYPE html>
<html>
  <head>
    <title>Shadow DOM</title>
    <style>
      #app {margin: 100px;}
      
    </style>
  </head>
  <body>
    <div id="app">
      <video src="http://www.w3school.com.cn/i/movie.ogg" controls="controls">
        your browser does not support the video tag
      </video>
    </div>
    
  </body>
</html>

3.1 使用偽類來控制 shadow-dom的樣式。

首先看如上程式碼顯示的效果圖如下:

在chrome瀏覽器下,我們檢視 shadow-dom 結構,可以看到每個元素都加上了一個pesudo 這樣的屬性。我們可以通過這些屬性使用偽類來控制他們的樣式。如下圖所示:

基本樣式如下:

video::-webkit-media-controls-play-button {
  background-color: red;
}

我們給播放按鈕新增了一個背景顏色為紅色。

注意:很遺憾的是,只有chrome瀏覽器下支援,其他瀏覽器下並不支援,雖然大部分瀏覽器下支援shadow-dom。

4. 使用javascript如何來建立Shadow DOM

4.1 使用 createShadowRoot()來建立Shadow DOM。

如下基本程式碼:

<!DOCTYPE html>
<html>
  <head>
    <title>Shadow DOM</title>
    <style>
      #app {margin: 100px;}
      .shadow-child {
        color: red;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="shadow-cls">hello, kongzhi</div>
    </div>
    <script type="text/javascript">
      // 1. 獲取影子宿主 shadow host
      var shadowHost = document.querySelector('.shadow-cls');
      // 2. 建立影子 shadow root
      var shadowRoot = shadowHost.createShadowRoot();
      // 3. shadow root 作為影子樹的第一個節點,其他的節點,比如如下的p節點都是它的子節點。
      shadowRoot.innerHTML = '<p class="shadow-child">我是子節點</p>';
    </script>
  </body>
</html>

然後我們檢視效果如下:

如上我們可以看到,給影子樹子節點設定css樣式,但是並沒有生效,那是因為影子宿主(shadow host)和影子根(shadow root)之間存在影子邊界。影子邊界保證DOM編寫的css和javascript程式碼都不會影響到ShadowDOM.當然反之也是一樣,互不影響。

4.2 理解<content> 和 <template> 的用法。

<content>標籤可以把來自DOM文件的內容新增到shadow DOM的內容被叫做分佈節點。

<content>有一個select屬性來告訴<content>標籤要插入的內容,select屬性值是一個使用CSS選擇器來獲取想要的內容。
選擇器包括類選擇器、元素選擇器等。

比如如下程式碼:

<!DOCTYPE html>
<html>
  <head>
    <title>Shadow DOM</title>
    <style>
      #app {margin: 100px;}
    </style>
  </head>
  <body>
    <div id="app">
      <div class="shadow-cls">
        <em class="shadowhost-content1">我是空智</em>
        <em class="shadowhost-content2">我是龍恩</em>
      </div>
      <!-- 下面是一個模板 template -->
      <template class="template">
        <div>
          <h1>哈哈,</h1>
          <content select=".shadowhost-content1"></content>
          我來了
          <content select=".shadowhost-content2"></content>!
        </div>
      </template>
    </div>
    <script type="text/javascript">
      // 1. 獲取影子宿主 shadow host
      var shadowHost = document.querySelector('.shadow-cls');
      // 2. 建立影子 shadow root
      var shadowRoot = shadowHost.createShadowRoot();
      // 3. 獲取模板元素
      var template = document.querySelector('.template');
      /*
       4. template.content 會返回一個文件片段。
       5. 使用 document.importNode獲取節點,true參數列示深度克隆
      */
      shadowRoot.appendChild(document.importNode(template.content, true));
    </script>
  </body>
</html>

執行結果如下:

我們也可以通過如下列印:

console.log(template.innerHTML);   // 獲取完整的HTML片段
console.log(template.content);  // 返回一個文件片段#document-fragment
console.log(template.childNodes);  // 返回[],說明childNodes無效

這些的資訊的區別,如下所示:

4.3 shadowDOM樣式

1. 宿主樣式:
在shadow DOM中利用 :host定義宿主的樣式。:host 是偽類選擇器,給所有的宿主新增樣式可以如下程式碼::host 或 :host(*); 如果想給單獨的宿主新增樣式可以 :host(xx); 其中xx是宿主的標籤或類選擇器等。並且:host還可以配合 :hover、:active等狀態來設定樣式,如下程式碼:

/* 定義宿主樣式 :host */
:host {
  color: red;
}
/* 定義宿主hover狀態下的樣式 */
:host(:hover) {
  color: black;
}

2. ::shadow

影子邊界為了保證DOM編寫的css或javascript不影響到shadowDOM。如果我們想讓 shadowDOM新增一些樣式,可以使用 ::shadow這樣的。

3. /deep/

::shadow選擇器有一個缺陷是它只能穿透一層影子邊界,如果我們在一個影子樹中巢狀了多個影子樹的話,那麼我們需要使用/deep/ 這樣的來編寫css樣式。

4. ::content

我們通過 <content> 標籤把主文件中的元素新增到shadowDOM的內容被叫做分佈節點。分佈節點的樣式渲染需要用到::content。比如我們想給節點為em標籤的話,我們直接寫 em {} 這樣的是不會生效的,我們要寫成 ::content > em {}. 比如如下程式碼:

/* 分佈節點的樣式渲染需要用到 ::content */
::content > em {
  color: blue;
  background: red;
}

下面我們再來看一下如下demo。

<!DOCTYPE html>
<html>
  <head>
    <title>Shadow DOM</title>
    <style>
      #app {margin: 100px;}
    </style>
  </head>
  <body>
    <div id="app">
      <div class="shadow-cls">
        <em class="shadowhost-content1">我是空智</em>
        <em class="shadowhost-content2">我是龍恩</em>
      </div>
      <!-- 下面是一個模板 template -->
      <template class="template">
        <style>
          /* 定義宿主樣式 :host */
          :host {
            color: red;
          }
          /* 定義宿主hover狀態下的樣式 */
          :host(:hover) {
            color: black;
          }
          /* 分佈節點的樣式渲染需要用到 ::content */
          ::content > em {
            color: blue;
            background: red;
          }
        </style>
        <div>
          <h1>哈哈,</h1>
          <content select=".shadowhost-content1"></content>
          我來了
          <content select=".shadowhost-content2"></content>!
        </div>
      </template>
    </div>
    <script type="text/javascript">

      // 1. 獲取影子宿主 shadow host
      var shadowHost = document.querySelector('.shadow-cls');
      // 2. 建立影子 shadow root
      var shadowRoot = shadowHost.createShadowRoot();
      // var shadowRoot = shadowHost.attachShadow({mode: 'open'});
      // 3. 獲取模板元素
      var template = document.querySelector('.template');
      /*
       4. template.content 會返回一個文件片段。
       5. 使用 document.importNode獲取節點,true參數列示深度克隆
      */
      shadowRoot.appendChild(document.importNode(template.content, true));
    </script>
  </body>
</html>

然後我們再在chrome瀏覽器下看下結果如下所示:

注意:上面的demo在chrome瀏覽器下會生效,在firefox或safari是不支援的。因此如果在平時的開發中我們需要引入對應的庫進來才會支援。

相關文章