带你走进->影子元素(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.

     这门技术可能暂时不必太深研究, 可是学会这门知识可使咱们有更广阔的技术视野, 不断学习老是会有用的, 此次就是这样, 但愿和你一块儿进步。

相关文章
相关标签/搜索