shadow-dom入门

认识 Shadow DOM

什么是 Shadow DOM

Shadow DOM 是 Web Components 定义的四大标准之一(Custom elements, Shadow DOM, HTML templates 和 HTML Imports)。

Shadow DOM是一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。这直接解决了HTML、CSS 和 JS 的全局性污染的问题。

Shadow DOM 结构表现

第一步:打开 Chrome 的开发者工具,点击右上角的“Settings”按钮,勾选“Show user agent shadow DOM”。
第二步:创建包含 video 元素的页面(audio, select, progress, input 等等 都行)

1.png

#shadow-root称为影子根,可以看到它在video里面,换句话说,#shadow-root寄生在video上,所以video此时称为影子宿主。可以看到上图有两个#shadow-root,这是因为#shadow-root可以嵌套,形成节点树,即称为影子树(shadow trees)。影子树对其中的内容进行了封装,有选择性的进行渲染。这就意味着我们可以插入文本、重新安排内容、添加样式等等。

3.png

Shadow DOM vs DOM vs Light DOM

tip: Light DOM 组件用户编写的标记,该 DOM 不在组件 shadow DOM 之内,它是元素实际的子项。
如:<button is="better-button"><img src="gear.svg" slot="icon"><span>Settings</span></button>

shadow DOM 有以下优点:

  1. 隔离 DOM:组件的 DOM 是独立的(例如,document.querySelector() 不会返回组件 Shadow DOM 中的节点)。

  2. 作用域 CSS:Shadow DOM 内部定义的 CSS 在其作用域内。样式规则不会泄漏,页面样式也不会渗入。

  3. 组合:为组件设计一个声明性、基于标记的 API。

  4. 简化 CSS: 作用域 DOM 意味着您可以使用简单的 CSS 选择器,更通用的 id/class 名称,而无需担心命名冲突。

  5. 效率:将应用看成是多个 DOM 块,而不是一个大的(全局性)页面。

使用 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
35
...
<style type="text/css">
#app {
--text-color: green;
}
</style>
<div id="app">
<h1>我是根元素</h1>
</div>
<script>
const app = document.getElementById('app');
const shadowRoot = app.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host {color: blue;}
h2 {color: var(--text-color, black);background: red;}
</style>
<h2>Hello Shadow DOM</h2>
`;
const span = document.createElement('span');
span.textContent = "你好";
shadowRoot.appendChild(span);

let css = document.createTextNode("h2{ font-size: 20px}");
const style = document.createElement('style')
style.appendChild(css)
shadowRoot.appendChild(style)

setTimeout(_ => {
span.textContent = "两秒后我改变了";
span.setAttribute('style', 'color: green;');
css = document.createTextNode("h2{ font-size: 30px}");
style.appendChild(css)
}, 2000)
</script>

在原生dom节点上创建shadowDOM,添加样式和内容,并进行更新。

注意点:

  1. 并不是所有的元素都可以挂载 Shadow DOM,其主要原因是:

    1. 浏览器已为该元素托管其自身的内部 shadow DOM(比如 textarea、input)。
    2. 让元素托管 shadow DOM 毫无意义 (比如 img)。
  2. Shadow DOM,一旦创建就无法删除,它只能用新的替换

  3. 在调用 attachShadow 创建 ShadowRoot 之后,attachShdow 方法会返回 ShadowRoot 对象实例

  4. mode 有 open 和 closed 选项,当为 closed 时无法获取shadowRoot属性

  5. 增加 Shadow DOM 后原父级子元素会无效

  6. 更新内容和样式都使用原生的JavaScript API

template 和 slot

前面说了,shadow dom可以实现dom的隔离,比如样式的封装,那么如何实现呢?shadow规定了一种名为template的标签,这种标签类似我们经常用的<script type='tpl'>,它不会被解析为dom树的一部分,template的内容可以被塞入到shadow dom中并且反复利用,在template中可以设置style,但只对这个template中的元素有效,看下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
span {
background-color:blue;/*设置页面所有span背景为蓝色,然而对shadow dom没什么卵用*/
}
</style>
<div id="con">
没什么卵用的文字
</div>
<template id="tpl">
<style>
span {
color:red;
}
</style>
<span>hello world</span>
</template>
1
2
3
4
5
6
var host = document.querySelector('#con');
var root = host.attachShadow({mode:'open'});

var con = document.getElementById("tpl").content.cloneNode(true);

root.appendChild(con);

可以看到,template的内容被塞入到宿主,并且其文案被设置为红色,而body 中对 span 设置为蓝色背景却没有生效;另外这里要注意document.getElementById(“tpl”).content中的content属性,它是template标签的特有属性,你可以通过嗅探该属性来判断浏览器是否支持shadow dom和template标签。这是shadow dom的组件化复用的基本方式。

由于shadow dom的内容会掩盖宿主的内容,那么现在问题来了,我就是想把宿主的内容显示出来怎么办?

这时就需要slot了,slot是一个插槽,一个坑位,可以在template中定义坑位,然后宿主中的内容可以标记属于哪一个坑位,这样一个萝卜一个坑,宿主的内容就会被正确地插入到template所标记的位置去,还是来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="con">
没什么卵用的文字
<span slot="main1">
坑位1
</span>
<span slot="main2">
坑位2
</span>
没什么卵用的文字 </div>
<template id="tpl">tpl begin
<slot name="main1">
</slot>
<slot name="main2">
</slot>
tpl end
</template>
1
2
3
4
5
6
var host = document.querySelector('#con');
var root = host.attachShadow({mode:'open'});

var con = document.getElementById("tpl").content.cloneNode(true);

root.appendChild(con);

看看,这就是Vue的solt插槽的方式。
宿主中的两个span分别插入到了其标记的slot坑位中。在slot出现之前,仍然可以实现类似的功能,只不过标签名叫content。

基于 Shadow DOM 的前端框架 omi —— 腾讯出品

  1. 基于 Shadow Dom 设计,Web Components + JSX + HTM 融合为一个框架 Omi
  2. Shadow DOM 与 Virtual DOM 融合,Omi 既使用了虚拟 DOM,也是使用真实 Shadow DOM,让视图更新更准确更迅速
  3. 局部 CSS 最佳解决方案(Shadow DOM),社区为局部 CSS 折腾了不少框架和库(使用js或json写样式,如:Radium,jsxstyle,react-style;与webpack绑定使用生成独特的className文件名—类名—hash值,如:CSS Modules,Vue),还有运行时注入scoped atrr 的方式,都是 hack 技术;Shadow DOM Style 是最完美的方案

…

参考

  1. Shadow DOM v1:独立的网络组件
  2. 神奇的Shadow DOM