Web components的一个重要特性是封装——可以将html标签结构、css样式和行为隐藏起来,并从页面上的其他代码中分离开来,这样不同的功能不会混在一起,代码看起来也会更加干净整洁。其中,Shadow DOM接口是关键所在,它可以将一个隐藏的、独立的DOM添加到一个元素上。本篇文章将会介绍 Shadow DOM的基础部分。

注意: Chrome 和 Opera 默认就支持 Shadow DOM。Firefox也很快就会支持。目前需要将dom.webcomponents.enableddom.webcomponents.shadowdom.enabled设置为true。Firefox将会在60/61版本中实现。Safari已经支持Shadow DOM了,而 Edge还在实现阶段。

概况

本文章假设你对DOM (文档对象模型) 有一定的了解,它是不同的元素节点、文本节点连接而成的一个树状结构,应用于标记文档中(例如 web文档中经常用到的HTML文档)。请看如下示例,一个 HTML片段:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Simple DOM example</title>
  </head>
  <body>
      <section>
        <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth.">
        <p>Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a></p>
      </section>
  </body>
</html>

这个片段会生成如下 DOM结构:

Shadow DOM允许将隐藏的DOM树添加到常规的DOM树中——它以shadow root为起始根节点,在这个根节点的下方,可以是任意元素,和普通的DOM元素一样。

 

需要了解的 Shadow DOM相关技术:

  • Shadow host: 一个常规 DOM节点,Shadow DOM会被添加到这个节点上。
  • Shadow tree:Shadow DOM内部的DOM树。
  • Shadow boundary:Shadow DOM结束的地方,也是常规 DOM开始的地方。
  • Shadow root: Shadow tree的根节点。

你可以使用同样的方式来操作Shadow DOM,就和操作常规DOM一样——例如添加子节点、设置属性,以及为节点添加自己的样式(例如通过 element.style.foo属性),或者为整个 Shadow DOM添加样式(例如在<style> 元素内添加样式)。不同的是,Shadow DOM内部的元素始终不会影响到它外部的元素,这为封装提供了便利。

注意,不管从哪个方面来看,Shadow DOM 都不是一个新事物——在过去的很长一段时间里,浏览器用它来封装一个元素的内部结构。以一个有着默认播放控制按钮的<video>元素为例。你所能看到的只是一个 <video>标签,实际上,在它的Shadow DOM中,包含来一系列的按钮和其他控制器。Shadow DOM标准允许你为你自己的元素(custom element)维护一组 Shadow DOM。

基本用法

可以使用Element.attachShadow() 方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个mode属性,值可以是open或者closed

let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});

open 表示你可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用Element.shadowRoot 属性:

let myShadowDom = myCustomElem.shadowRoot;

如果你将一个 Shadow root 添加到一个 Custom element 上,并且将 mode设置为closed,那么就不可以在外部获取 Shadow DOM了——myCustomElem.shadowRoot 将会返回 null。浏览器中的某些内置元素就是这样的,例如<video>,包含了不可访问的 Shadow DOM。

注意: 正如这篇文章所阐述的,处理 closed Shadow DOMs 实际上很简单,往往也很值得这么做。

如果你想将一个Shadow DOM添加到 custom element上,可以在 custom element的构造函数添加如下实现(这往往也是最有用的做法):

let shadow = this.attachShadow({mode: 'open'});

当你将一个 Shadow DOM添加到一个元素上,那么之后,就可以使用 DOM APIs 对它进行操作,就和处理常规DOM一样。

var para = document.createElement('p');
shadow.appendChild(para);
etc.

编写简单示例

现在,让我们着手实现一个示例——<popup-info-box>(也可以查看在线示例),来说明 Shadow DOM在 custom element 中的实际运用。它包含一个图片 icon和一段文字,这个图片 icon用于在页面上显示。每当 icon获取到焦点时,文字会在一个弹框中显示,以提供更加详细的信息。首先,在 JavaScript文件中,我们需要定义一个叫做 PopUpInfo的类,它继承自HTMLElement

class PopUpInfo extends HTMLElement {
  constructor() {
    // 必须首先调用 super方法
    super();

    // 元素的具体功能写在下面

    ...
  }
}

在上面的类中,我们将会在它的构造函数中定义元素所有的功能。当类实例化后,所有的实例元素都会有相同功能。

创建 shadow root

在构造函数中,我们首先将 Shadow root 添加到 custom element上:

// 创建 shadow root
var shadow = this.attachShadow({mode: 'open'});

创建 shadow DOM 结构

接下来,我们会使用相关DOM 操作来创建元素的 Shadow DOM结构:

// 创建 span
var wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
var icon = document.createElement('span');
icon.setAttribute('class','icon');
icon.setAttribute('tabindex', 0);
var info = document.createElement('span');
info.setAttribute('class','info');

// 获取属性的内容并将内容添加到 info元素内
var text = this.getAttribute('text');
info.textContent = text;

// 插入 icon
var imgUrl;
if(this.hasAttribute('img')) {
  imgUrl = this.getAttribute('img');
} else {
  imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);

为 shadow DOM 添加样式

之后,我们将要创建<style>元素,并加入一些 CSS样式:

// 为 shadow dom添加一些 CSS样式
var style = document.createElement('style');

style.textContent = '.wrapper {' +
                           'position: relative;' +
                        '}' +

                         '.info {' +
                            'font-size: 0.8rem;' +
                            'width: 200px;' +
                            'display: inline-block;' +
                            'border: 1px solid black;' +
                            'padding: 10px;' +
                            'background: white;' +
                            'border-radius: 10px;' +
                            'opacity: 0;' +
                            'transition: 0.6s all;' +
                            'position: absolute;' +
                            'bottom: 20px;' +
                            'left: 10px;' +
                            'z-index: 3;' +
                          '}' +

                          'img {' +
                            'width: 1.2rem' +
                          '}' +

                          '.icon:hover + .info, .icon:focus + .info {' +
                            'opacity: 1;' +
                          '}';

将 Shadow DOM添加到 Shadow root上

最后,将所有创建的元素添加到 Shadow root上:

// 将所创建的元素添加到 Shadow DOM上
shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

使用我们的 custom element

完成类的定义之后,使用元素也是同样的简单,只需将 custom element放在页面上,正如Using custom elements中讲解的那样:

// 定义新的元素
customElements.define('popup-info', PopUpInfo);
<popup-info img="img/alt.png" text="Your card validation code (CVC) is an extra 
                                    security feature — it is the last 3 or 4
                                    numbers on the back of your card.">
 

文档标签和贡献者

此页面的贡献者: haoliangwu, zhang-quan-yi
最后编辑者: haoliangwu,