【MDN翻译】shadow DOM的使用

1,254 阅读4分钟

欢迎关注微信公众号:前端阅读室

web components的一个重要方面就是封装,这样才能保持标签结构、样式、行为和页面中的其它代码隔离。Shadow DOM API就是封装很关键的一部分,它为元素提供了隐藏、隔离DOM的方式。

注:Shadow DOM被Firefox >=63, Chrome, Opera, and Safari,Edge >=75支持。

概览

Shadow DOM允许你在常规的DOM树中隐藏Shadow DOM,Shadow DOM的根节点是shadow root,shadow root里可以附加任何DOM元素,和常规DOM没有区别。

下面我们介绍几个shadow DOM的术语:

  • Shadow host(宿主):附加了shadow DOM的常规DOM
  • Shadow tree:shadow DOM里面的DOM树
  • Shadow boundary(边界):shadow DOM结束,常规DOM开始的位置
  • Shadow root:Shadow tree的根节点

你可以像操作常规DOM一样操作shadow DOM里面的DOM。比如添加子节点、设置属性、样式,或者在shadow DOM添加<style>标签为整个shadow DOM设置样式。shadow DOM的不同之处在于,shadow DOM里面所有的代码操作都不会影响外面,所以对封装很有帮助。

shadow DOM其实不是新东西,在浏览器中其实已经使用很长时间了,浏览器使用它来封装内置DOM元素的结构,比如<video>元素,在它的shadow DOM里面就包含了一系列的button以及其一些其他控件。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;

如果mode=closed,你是无法在shadow DOM外面获取到shadow root的,myCustomElem.shadowRoot会返回null。比如,内置的<video>元素,它的设置就是mode=closed,所以video.shadowRoot返回的就是null。

如果你想让shadow DOM封装的自定义元素成为应用的一部分,可以自由访问,你需要设置mode=open。

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

操作shadow DOM和操作常规DOM是相似的,你可以使用相同的DOM API。

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

简单例子

现在我们通过一个简单例子来演示一下如果使用shadow DOM来封装一个自定义元素——<popup-info>。这个元素有一张图片和一段文本,当focus在上面的时候,它会提示一些信息。

我们在JS文件中定义一个叫PopUpInfo的class,它继承了HTMLElement。

class PopUpInfo extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

    // write element functionality in here
  }
}

然后,创建shadow root

class PopUpInfo extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

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

创建shadow DOM的结构

class PopUpInfo extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

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

    // Create spans
    let wrapper = document.createElement("span");
    wrapper.setAttribute("class", "wrapper");
    let icon = document.createElement("span");
    icon.setAttribute("class", "icon");
    icon.setAttribute("tabindex", 0);
    let info = document.createElement("span");
    info.setAttribute("class", "info");

    // Take attribute content and put it inside the info span
    let text = this.getAttribute("data-text");
    info.textContent = text;

    // Insert icon
    let imgUrl;
    if (this.hasAttribute("img")) {
      imgUrl = this.getAttribute("img");
    } else {
      imgUrl = "img/default.png";
    }
    let img = document.createElement("img");
    img.src = imgUrl;
    icon.appendChild(img);

    // write element functionality in here
  }
}

接下来,我们创建一个<style>标签为shadow DOM设置一些样式

// Create some CSS to apply to the shadow dom
let 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中

// attach the created elements to the shadow dom
shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

我们使用customElements.define定义这个新元素。

// Define the new element
customElements.define('popup-info', PopUpInfo);

最后就可以在页面中使用它了

<popup-info
  img="img/alt.png"
  data-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."
>

内部样式vs外部样式

在例子中我们使用<style/>给Shadow DOM设置样式,不过更好的方式可能是使用<link>引入外部样式。

// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');

// Attach the created element to the shadow dom
shadow.appendChild(linkElem);

注:由于link的样式需要加载,所以可能会出现FOUC(flash of unstyled content )。

许多现代浏览器对<style>做了优化。如果是相同的节点,<style>可以克隆一份。如果是相同的内容,可以复用相同的样式。由于这些优化外部样式和内部样式的性能已经很接近了。

本文翻译自

欢迎关注微信公众号:前端阅读室