shadow-DOM

shadow DOM的使用

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。

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

open意味着你可以在页面中使用JavaScript访问shadow DOM,比如使用Element.shadowRoot属性。

1
let myShadowDom = myCustomElem.shadowRoot;

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

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

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

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

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

简单例子

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

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

1
2
3
4
5
6
7
8
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();

// write element functionality in here
}
}

然后,创建shadow root

1
2
3
4
5
6
7
8
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();

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

创建shadow DOM的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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设置一些样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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中

1
2
3
4
5
// attach the created elements to the shadow dom
shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

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

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

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

1
2
3
4
5
6
<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>引入外部样式。

1
2
3
4
5
6
7
// 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>可以克隆一份。如果是相同的内容,可以复用相同的样式。由于这些优化外部样式和内部样式的性能已经很接近了。

本文翻译自

0%