影子节点ShadowDOM

4,627 阅读7分钟

shadow DOM是什么

<video controls autoplay name="media">
  <source id="mp4" src="trailer.mp4" type="video/mp4">
</video>

  上面这是最简单的视屏标签,里面有默认的音量等按键。在源代码中根本没有一点痕迹。那这些节点是从哪里来的?
  这就是shadow DOM,视屏的控件在浏览器中真实面目如下:

shadow DOM浏览器中体现
shadow DOM浏览器中体现

  发现#shadow-root是灰色的,这是浏览器为了表明在shadow DOM中,代表页面其他的部分的内容不会对内部产生影响(可用特定方式穿透,后文会说到)

内容具体指css选择器和javascript代码

  简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装HTML组件(类似vue组件,将html,css,js独立部分提取)。

为什么要使用shadow DOM

  Bootstrap的名字你一定不陌生,代码一般如下:

<ul class="media-list">
  <li class="media">
    <div class="media-left">
       <a href="#">
        ![](...)
       </a>
    </div>
    <div class="media-body">
       <h4 class="media-heading">Media heading</h4>
    </div>
  </li>
</ul>

  非常的简单好用,但是这些东西你并没有深入了解,往往结构变复杂之后,都是一堆模板,修改是一个很难的问题,牵一发而动全身。
  这种情况下shadow DOM的优势十分巨大,你可以这么写模板

// 我是一个简洁的模板
<bootstrap-media-list>
  <a href="#">
    ![](...)
  </a>
  <h4 class="media-heading">Media heading</h4>
</bootstrap-media-list>

  当然想实现这么写还需要一些js,css配合才行

如何使用

先运行一个例子

<div class="widget">Hello, world!</div>
<script>
  var host = document.querySelector('.widget');
  var root = host.createShadowRoot();
  root.textContent = '我在你的 div 里!';
</script>

运行结果
运行结果

  首先我们指定一个宿主节点shadow host)然后创建影子根(shadow root)为它添加一个文本节点,结果宿主中的内容未被渲染。

如何渲染宿主节点中的内容

  只渲染影子根中的内容基本没有实用的地方,但能自由的渲染宿主节点中的内容的话就可以让页面展现更灵活。我们需要content标签

<div class="pokemon">胖丁</div>
<template class="pokemon-template">
  <h1>一只野生的<content></content>出现了!</h1>
</template>
<script>
  var host = document.querySelector('.pokemon');
  var root = host.createShadowRoot();
  var template = document.querySelector('.pokemon-template');
  root.appendChild(document.importNode(template.content, true)); 
</script>


  <content>标签创建了一个插入点 .pokemon里面的文本投影出来,多个内容匹配时可以实用select属性指定

<div class="host">
    <p>大慈大悲,由诸葛亮进化而来。</p>
    <span class="name">观音姐姐兽</span>
</div>
<template class="root-template">
    <dl>
      <dt>名字</dt>
      <dd><content select=".name"></content></dd>
    </dl>
    <p><content select=""></content></p>
</template>
<script>
    var host = document.querySelector('.host');
    var root = host.createShadowRoot();
    var template = document.querySelector('.root-template');
    root.appendChild(template.content);
</script>


  可以使用select属性类似选择器的形式渲染宿主节点中匹配元素的投影。这种形式不但可以改变DOM流的顺序也可以让布局变得灵活。
  在模板的最后<content select=""></content>是一种贪心匹配,把宿主节点中所有未被匹配的内容全部投影。需要注意的是把贪心匹配放在最前面会把所有的节点投影并且之后的select不会再获取到被其投影的内容。
  以下都是等效的:

  • <content></content>
  • <conent select=""></conent>
  • <content select="*"></content>

样式渲染与封装

  先看一个简单的例子

<style>
    button {
        font-size: 18px;
        font-family: '华文行楷';
    }
</style>
<button>普通按钮</button>
<div></div>
<script>
    var host = document.querySelector('div');
    var root = host.createShadowRoot();
    root.innerHTML = 
      '<style>button { font-size: 24px; color: blue; } </style>'+
      '<button>影子按钮</button>';
</script>


  在影子节点中存在边界使shadow DOM样式和正常DOM流中的样式不相互干扰。这是一种作用域化的体现,不用再担心样式的相互冲突。

(:host)选择器

  :host是伪类选择器选择宿主节点,我们可以扩展一下上面的例子

<style>
    p {
        font-size: 12px;
    }
</style>
<p>我的文本</p>
<button>我的按钮</button>
<template class="shadow-template">
    <style>
        :host(p) {
            color: green;
        }
        :host(button) {
            color: red;
        }
        :host(*) {
            font-size: 24px;
        }
    </style>
    <content select=""></content>
</template>
<script>
    var root1 = document.querySelector('p').createShadowRoot();
    var root2 = document.querySelector('button').createShadowRoot();

    var template = document.querySelector('.shadow-template');

    root1.appendChild(document.importNode(template.content, true));
    root2.appendChild(document.importNode(template.content, true));
</script>


  这个例子有几个点:

  • p标签字体大小是12px = 影子样式的优先级不如页面样式
  • :host选择器中可以使用任意合法选择器,*应用于所有
  • 通过挂载不同宿主渲染出不同的内容,可以实现主题化
      上面的主题化并不完全,只根据挂载元素进行选择也就是说.parent > .child,但是我们还能通过:host-context实现.parent < .child如下
    <div class="serious">
      <p class="serious-widget">
          serious-widget
      </p>
    </div>
    <div class="playful">
      <p class="playful-widget">
          playful-widget
      </p>
    </div>
    <template class="widget-template">
      <style>
          :host-context(.serious) {
              width: 250px;
              height: 50px;
              background: tomato;
          }
          :host-context(.playful) {
              width: 250px;
              height: 50px;
              background: deepskyblue;
          }
      </style>
      <content></content>
    </template>
    <script>
    var root1 = document.querySelector('.serious-widget').createShadowRoot();
    var root2 = document.querySelector('.playful-widget').createShadowRoot();
    var template = document.querySelector('.widget-template');
    root1.appendChild(document.importNode(template.content, true));
    root2.appendChild(document.importNode(template.content, true));
    </script>


  上面的效果就非常不错了,可以进行动态组件构建

ps: 伪类,伪元素选择器也可以直接使用,效果和正常节点中一致

(::content)选择器

  在使用 shadow DOM 的时候应该确保内容和表现的分离,也就是说文本应该来自页面而不是埋在 shadow DOM 的模板里。所以我们需要在模板中对分布式节点进行渲染。

<div class="widget">
    <button>分布节点碉堡啦!</button>
</div>
<template class="widget-template">
    <style>
        ::content > button {
            color: white;
            background: tomato;
            border-radius: 10px;
            border: none;
            padding: 10px;
        }
    </style>
    <content select=""></content>
</template>
<script>
var root = document.querySelector('.widget').createShadowRoot();
var template = document.querySelector('.widget-template');
root.appendChild(document.importNode(template.content, true));
</script>

打破作用域(::shadow)

  我们可以在挂载节点中使用::shadow,比如

<style>
    .sign-up::shadow #username{
        font-size: 20px;
        border: 1px solid red;
    }
</style>
<div class="sign-up"></div>
<template class="sign-up-template">
    <style>
        #username{
            font-size: 12px;
        }
    </style>
    <div>
        <input type="text" id="username" placeholder="用户名">
    </div>
</template>
<script>
var root = document.querySelector('.sign-up').createShadowRoot();
var template = document.querySelector('.sign-up-template');
root.appendChild(document.importNode(template.content, true));
</script>


  不过缺点是只能穿透一层,但我们还有一个神器!

多层穿透(/deep/)

<style>
    #foo /deep/ button {
        color: red;
    }
</style>
<div id="foo"></div>
<template>
    <div id="bar"></div>
</template>
<script>
    var root1 = document.querySelector('#foo').createShadowRoot();
    var template = document.querySelector('template');
    root1.appendChild(document.importNode(template.content, true));
    var root2 = root1.querySelector('#bar').createShadowRoot();
    root2.innerHTML = '<button>点我点我</button>';
</script>


javascript的区别

  1. 数据并没有块级化,仍挂载在window
  • 事件重定向(原来绑定在 shadow DOM 节点中的事件被重定向了,所以他们看起来像绑定在宿主节点上一样)
    <input id="normal-text" type="text" value="I'm normal text">
    <div id="host"></div>
    <template>
      <input id="shadow-text" type="text" value="I'm shadow text">
    </template>
    <script>
      var root = document.querySelector('#host').createShadowRoot();
      var template = document.querySelector('template');
      root.appendChild(document.importNode(template.content, true));
      document.addEventListener('click', function(e) {
        console.log(e.target.id + ' clicked!');
      });
    </script>

  可以看到在影子节点的事件被宿主节点代理。

事件阻塞

  在监听以下事件时会被阻塞在影子节点的根:

  • aborterror
  • select
  • change
  • load
  • reset
  • reset
  • resize
  • scroll
  • selectstar
    <input id="normal-text" type="text" value="I'm normal text">
    <div id="host">
      <input id="distributed-text" type="text" value="I'm distributed text">
    </div>
    <template>
      <div><content></content></div>
      <div>
          <input id="shadow-text" type="text" value="I'm shadow text">
      </div>
    </template>
    <script>
      var root = document.querySelector('#host').createShadowRoot();
      var template = document.querySelector('template');
      root.appendChild(document.importNode(template.content, true));
      document.addEventListener('select', function(e) {
        console.log(e.target.id + ' text selected!');
      });
    </script>

  事件影子节点的根上被阻止,无法冒泡到ducoment,所以无法监听。

分布节点

  分布节点指之前通过<content>标签将宿主节点的内容投影,分布节点不会发生上面的阻塞情况,因为这个只是一个投影实际的内容还是挂载在宿主节点上。