帶你走進->影子元素(Shadow DOM)&瀏覽器原生組件開發(Web Components API )

帶你走進->影子元素(Shadow DOM)&瀏覽器原生組件開發(Web Components API )

image.png

本篇介紹

    習慣了使用vuereact等框架來開發組件, 但其實咱們能夠不依賴任何框架, 直接原生開發組件, 因此這個原生api的一大優勢就是能夠不依賴任何的框架。css

    瀏覽器自己支持組件是大趨勢, 可是目前使用起來並不夠好, 但這並不能阻擋咱們學習的腳步與對知識的好奇心, 並且我也相信原生組件幾年後會成爲一種主流的組件編寫方式, 如今就讓咱們一塊兒來學習它吧。html

1. 兼容性

Chrome 54 Safari 10.1 Firefox 63

MDN上顯示:
image.png前端

    不建議直接上生產環境。vue

2. 影子元素

     還記得是我第一次用qiankun.js框架的時候看到的這個概念(接下來的文章會寫微前端相關實戰), 這個技術能夠實現一部分的css樣式隔離, 之因此說只是實現一部分樣式隔離, 學完這篇文章你就懂了。html5

第一步: 生成影子元素

    咱們新建一個html5頁面, 寫上以下結構react

<!DOCTYPE html>
<html lang="en">
<head>
  <style>
    #cc-shadow {
      margin: auto;
      border: 1px solid #ccc;
      width: 200px;
      height: 200px;
    }
  </style>
</head>
<body>
  <div id="cc-shadow">
     <span>我是內部元素</span>
  </div>
  <script>
   const oShadow = document.getElementById("cc-shadow");
   const shadow = oShadow.attachShadow({mode: 'open'});  
 </script>
</body>
</html>

    奇怪的一幕出現了, 內部元素不可見而且在查看結構的控制檯裏出現了特殊的結構定義。
image.pngweb

  1. attachShadow方法給指定的元素掛載一個Shadow DOM
  2. mode: open 表示能夠經過頁面內的 JavaScript 方法來獲取 Shadow DOM。
  3. mode: open針對是dom.shadowRoot方法, 直接getElementsByClassName獲取仍是能夠獲取到的(這條很重要, 有的文章都說錯了)。
  4. mode: open對應的是mode: close
  5. 注意: 不能夠先開後關這種操做
第二步: 往裏面注入元素
const link = document.createElement("a");
    link.href = 'xxxxxxxxxxxx';
    link.innerHTML =  '點我跳轉';
    shadow`.appendChild(link);
  1. 注意這裏使用的是shadow, 而不是dom自己。
第三步: 往裏面注入樣式
const styles = document.createElement("style");
 styles.textContent = `* { color:red  } `
 shadow.appendChild(styles);
  1. 經過上面能夠看出, 建立了一個style標籤插入了進去。
  2. 與此相似咱們能夠建立一個link標籤插入進來效果也是同樣的。

效果以下:
image.pngapi

第四步: 樣式隔離實驗
styles.textContent = `
       * { color:red  } 
       body {
         background-color: red;
       }
    `

    這裏咱們在影子元素內部改變了body的樣式, 而這個樣式沒有做用到外面的body身上。
image.png瀏覽器

第五步: 樣式滲透實驗

    經過上面操做你是否是感受這個沙盒能完美隔離css了? 那咱們如今對最外層的style標籤裏面加上字體大小的樣式, 由於影子元素沒法隔離可繼承的樣式。app

* {
    font-size: 40px;
  }

效果以下:
image.png

總結一下:

     影子元素確實能夠防止樣式泄露到外面污染全局, 可是也無法避免被全局的樣式滲透污染, 這裏的滲透指的是可繼承的樣式, 好比你外面style用id獲取影子裏面的元素改變border屬性那就是無效的, 因此qiankun.js暫時沒法完美隔離樣式, 好比想要改變全局樣式就須要靠js幫忙。
     有了上述的知識儲備, 就讓咱們來迎接下一位主角原生組件

完整代碼(來複制玩玩吧):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #cc-shadow {
      margin: auto;
      border: 1px solid #ccc;
      width: 200px;
      height: 200px;
    }
    * {
      font-size: 40px;
    }
  </style>
</head>
<body>
  <div id="cc-shadow">
    <span>我是內部元素</span>
  </div>
  <script>
    // 1: 生成影子元素
    const oShadow = document.getElementById("cc-shadow");
    const shadow = oShadow.attachShadow({ mode: 'open' });
    // 2: 注入元素
    const link = document.createElement("a");
    link.href = 'xxxxxxxxxxxx';
    link.innerHTML = '點我跳轉';
    shadow.appendChild(link);
    // 3: 輸入樣式
    const styles = document.createElement("style");
    styles.textContent = `
       * { color:red  } 
       body {
         background-color: red;
       }
    `
    // 4: 插入使用,  可使用插入link的方式插入css, 效果相同
    shadow.appendChild(styles);
  </script>
</body>
</html>

3. 原生組件的使用

    下圖是我作的一個原生組件, 而且附上了使用方法。
image.png

<cc-mw name="大魔王1" image="../imgs/利姆露.webp"/></cc-mw>
 <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>

上面組件的使用看起來與vue等框架裏面的組件差很少, 可是它但是很嬌氣的!

注意事項
  1. 自定義元素的名稱必須包含連詞線,用與區別原生的 HTML 元素。因此,<cc-mw>不能寫成<ccMw>
  2. 若是以下方式書寫去掉結尾閉合標籤, 只會顯示第一個, 第二個沒有被渲染(這個真的好奇怪), 第二個組件會默認被插到第一個組件中, 因爲被插入影子元素因此不顯示了。

    <cc-mw name="大魔王1" image="../imgs/利姆露.webp"/>
    <cc-mw name="大魔王2" image="../imgs/利姆露.webp"/>

    image.png

    奇奇怪怪的現象不是此次的主題, 咱們繼續研究乾貨。

4. 編寫組件第一步template

template裏面的dom結構就至關於影子元素的結構內部。

<template id="ccmw">
    <style>
      :host {
        border: 1px solid red;
        width: 200px;
        margin-bottom: 10px;
        display: block;
        overflow: hidden;
      }

      .image {
        width: 70px;
        height: 70px;
      }

      .container {
        border: 1px solid blue;
      }

      .container>.name {
        font-size: 20px;
        margin-bottom: 5px;
      }
    </style>

    <img class="image">
    <div class="container">
      <p class="name"></p>
    </div>
  </template>

知識點逐一解釋:

第一個: dom定義

    上面代碼咱們拉倒最下面, 在這裏咱們能夠正常的定義dom, 放心書寫吧與外面寫法同樣。

第二個: 定義id

     <template id="ccmw">這句是讓寫咱們能夠找到這個模板。

第三個: <style>標籤

    咱們能夠當成template標籤內部就是一個影子元素的結構內部, 因此這裏能夠插入樣式標籤, 並不用js協助。

第四個: :host

    選擇包含使用這段 CSS 的Shadow DOM的影子宿主, 也就是組件的外殼父元素。

5. 組件類

    編寫一個組件固然須要邏輯代碼啦, 該js閃亮出場了。

<script>
    class CcMw extends HTMLElement {
      constructor() {
        super();
        var shadow = this.attachShadow({ mode: 'closed' });
        var templateElem = document.getElementById('ccmw');
        var content = templateElem.content.cloneNode(true);
        content.querySelector('img').setAttribute('src', this.getAttribute('image'));
        content.querySelector('.container>.name').innerText = this.getAttribute('name');
        shadow.appendChild(content);
      }
    }
    window.customElements.define('cc-mw', CcMw);
  </script>

知識點逐一解釋:

第一個: HTMLElement

截取w3school上面的定義, 由此可知這個父類賦予了組件dom元素的基礎屬性。
image.png

第二個: 老朋友attachShadow

    把dom變成影子容器, 這樣組件就能夠獨立出來了。

第三個: templateElem.content.cloneNode(true)

    克隆出模板裏的元素, 之因此是克隆由於組件會被複用。

第四個: window.customElements.define('cc-mw', CcMw);

     組件名類名相互綁定, 官方的話就是該對象可用於註冊新的自定義元素並獲取有關之前註冊的自定義元素的信息

第五個: 組件內部獲取外部元素

    組件內是能夠獲取大外部元素的, 因此能夠對全局進行操做, 要慎用哦。

    咱們甚至能夠直接把組件插入到 body中, 請注意容許, 但不提倡。

第六個: this是誰

    this就是元素自己啦。
image.png

學完影子元素是否是就很輕鬆理解上面的操做都是在幹嗎了, 開不開心。

附上完整代碼你們一塊兒玩玩:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>原生組件</title>
  <style>
    /* 不會影響內部的樣式 */
    .name {
      border: 2px solid red;
    }
  </style>
</head>

<body>
  <cc-mw name="大魔王1" image="../imgs/利姆露.webp"/></cc-mw>
  <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>
  
  <template id="ccmw">
    <style>
      :host {
        display: block;
        overflow: hidden;
        border: 1px solid red;
        width: 200px;
        margin-bottom: 10px;
      }

      .image {
        width: 70px;
        height: 70px;
      }

      .container {
        border: 1px solid blue;
      }

      .container>.name {
        font-size: 20px;
        margin-bottom: 5px;
      }
    </style>

    <img class="image">
    <div class="container">
      <p class="name"></p>
    </div>
  </template>

  <script>
    class CcMw extends HTMLElement {
      constructor() {
        super();
        var shadow = this.attachShadow({ mode: 'closed' });
        var templateElem = document.getElementById('ccmw');
        var content = templateElem.content.cloneNode(true);
        content.querySelector('img').setAttribute('src', this.getAttribute('image'));
        content.querySelector('.container>.name').innerText = this.getAttribute('name');
        shadow.appendChild(content);
      }
    }
    window.customElements.define('cc-mw', CcMw);
  </script>
</body>
</html>

6. 集成爲一個js文件

     上面的代碼有個問題, 就是怎麼組件代碼與業務代碼放在了一塊兒, 固然咱們能夠經過技巧把他們拆散, 這裏使用的是模板字符串js生成模板動態插入。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>原生組件</title>
  <style>
    /* 不會影響內部的樣式 */
    .name {
      border: 2px solid red;
    }
  </style>
</head>

<body>
  <cc-mw name="大魔王1" image="../imgs/利姆露.webp"/></cc-mw>
  <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>
  
  <script src="./2.拆散.js"></script>
</body>
</html>

上面代碼清爽了不少, 下面咱們能夠專心寫一個插件了:
./2.拆散.js

const template = document.createElement('template');
 
template.innerHTML = `
  <style>
      :host {
        border: 2px solid red;
        width: 200px;
        margin-bottom: 10px;
        display: block;
        overflow: hidden;
      }

      .image {
        width: 70px;
        height: 70px;
      }

      .container {
        border: 1px solid blue;
      }

      .container>.name {
        font-size: 20px;
        margin-bottom: 5px;
      }
    </style>

    <img class="image">
    <div class="container">
      <p class="name"></p>
    </div>
`

class CcMw extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow({ mode: 'closed' });
    var content = template.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    shadow.appendChild(content);
  }
}
window.customElements.define('cc-mw', CcMw);

7. 動態修改數據

     不能修改數據怎麼能叫組件那, 這裏咱們要利用類的方法。

組件類添加方法:
class UserCard extends HTMLElement {
  constructor() {
    // ...
    this.oName = content.querySelector('.container>.name');
    // ...
    shadow.appendChild(content);
  }
  // 添加方法動態改變name
  changeName(name){
    this.oName.innerText = name
  }
}

咱們在使用組件的頁面使用以下代碼:(注意: 這裏爲第一個組件加了id)

<cc-mw name="大魔王1" id="mw" image="../imgs/利姆露.webp"/></cc-mw>
 <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>
 
 <script src="./2.拆散.js"></script>
 
 <script>
     const mw = document.getElementById('mw');
     setTimeout(()=>{
       mw.changeName('修改後的魔王');
     }, 1000)
  </script>

image.png

其餘的修改方法其實就一模一樣了。

8. slot插槽

在模板代碼裏面加上: (若是不傳就顯示默認文案)

<div class="container">
      <p class="name"></p>
      <slot name="msg">默認文案</slot>
    </div>

使用的時候:

<cc-mw name="大魔王1" id="mw" image="../imgs/利姆露.webp"/>
     <span slot="msg">進化了</span>
</cc-mw>
<cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>

效果以下:

image.png

end.

     這門技術可能暫時不必太深研究, 可是學會這門知識可使咱們有更廣闊的技術視野, 不斷學習老是會有用的, 此次就是這樣, 但願和你一塊兒進步。

相關文章
相關標籤/搜索