乐趣区

关于前端:带你走进影子元素Shadow-DOM浏览器原生组件开发Web-Components-API

带你走进 -> 影子元素(Shadow DOM)& 浏览器原生组件开发(Web Components API)

本篇介绍

    习惯了应用 vuereact等框架来开发组件, 但其实咱们能够不依赖任何框架, 间接原生开发组件, 所以这个原生 api 的一大长处就是能够不依赖任何的框架。

    浏览器自身反对组件是大趋势, 然而目前应用起来并不够好, 但这并不能阻挡咱们学习的脚步与对常识的好奇心, 而且我也置信原生组件几年后会成为一种支流的组件编写形式, 当初就让咱们一起来学习它吧。

1. 兼容性

Chrome 54 Safari 10.1 Firefox 63

MDN 上显示:

    不倡议间接上生产环境。

2. 影子元素

     还记得是我第一次用 qiankun.js 框架的时候看到的这个概念 (接下来的文章会写 微前端 相干实战), 这个技术能够实现一部分的css 款式隔离, 之所以说只是实现一部分款式隔离, 学完这篇文章你就懂了。

第一步: 生成影子元素

    咱们新建一个 html5 页面, 写上如下构造

<!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>

    奇怪的一幕呈现了, 外部元素不可见并且在查看构造的控制台里呈现了非凡的构造定义。

  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 标签 插入进来成果也是一样的。

成果如下:

第四步: 款式隔离试验
 styles.textContent = `
       * {color:red} 
       body {background-color: red;}
    `

    这里咱们在影子元素外部扭转了 body 的款式, 而这个款式没有作用到里面的 body 身上。

第五步: 款式浸透试验

    通过下面操作你是不是感觉这个沙盒能完满隔离 css 了? 那咱们当初对最外层的 style 标签外面加上字体大小的款式, 因为 影子元素 无奈隔离可继承的款式。

* {font-size: 40px;}

成果如下:

总结一下:

     影子元素的确能够避免款式泄露到里面净化全局, 然而也没法防止被全局的款式浸透净化, 这里的浸透指的是可继承的款式, 比方你里面 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. 原生组件的应用

    下图是我做的一个 原生组件, 并且附上了应用办法。

 <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"/>

    奇奇怪怪的景象不是这次的主题, 咱们持续钻研干货。

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 元素的根底属性。

第二个: 老朋友attachShadow

    把 dom 变成影子容器, 这样组件就能够独立进去了。

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

    克隆出 模板里的元素, 之所以是克隆因为组件会被复用。

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

     组件名 类名 互相绑定, 官网的话就是 该对象可用于注册新的自定义元素并获取无关以前注册的自定义元素的信息

第五个: 组件外部获取内部元素

    组件内是能够获取大内部元素的, 所以能够对全局进行操作, 要慎用哦。

    咱们甚至能够间接把组件插入到 body中, 请留神容许, 但不提倡。

第六个: this是谁

    this就是元素自身啦。

学完 影子元素 是不是就很轻松了解下面的操作都是在干嘛了, 开不开心。

附上残缺代码大家一起玩玩:

<!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>

其余的批改办法其实就一模一样了。

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>

成果如下:

end.

     这门技术可能临时没必要太深钻研, 然而学会这门常识能够使咱们有更广大的技术视线, 一直学习总是会有用的, 这次就是这样, 心愿和你一起提高。

退出移动版