🍘 Shadow DOM 影子 DOM
Shadow DOM 为封装而生。它可以让一个组件拥有自己的「影子」DOM 树,这个 DOM 树 不能在主文档中被任意访问,可能拥有 局部样式规则,还有其他特性。
# 内建的 Shadow DOM
🌰 例子:
例如这个内建的范围选择器。浏览器在内部使用 DOM/CSS 来绘制它们。这个 DOM 结构一般来说对我们是隐藏的。但可以在开发者工具里面看见它。比如,在 Chrome 里,需要打开「Show user agent shadow DOM」选项。 #shadow-root
下的内容就是 影子 DOM。
对于影子 DOM 元素,不能使用 一般的 JavaScript 或者 CSS 选择器来获取,因为它们不是常规的元素。
- 有自己的 id 空间。
- 对主文档的 JavaScript 选择器隐身,比如
querySelector
。 - 只使用 shadow tree 内部的样式,不使用主文档的样式。
# Shadow DOM tree
一个 DOM 元素可以有以下两类 DOM 子树:
light tree
:常规 DOM 子树,由 HTML 子元素组成。shadow tree
:隐藏的 DOM 子树,不在 HTML 中反映,无法被察觉。
如果一个元素同时有以上两种子树,那么浏览器只渲染 shadow ,但是同样可以设置两种树的组合。
影子树可以在自定义元素中使用,作用是隐藏组件内部结构和添加只在组件内有效的样式。
使用 element.attachShadaw({mode: ...})
创建一个影子树。
两个限制:
- 每个元素只能创建一个影子树;
- 元素必须是 自定义元素 或者下列元素之一:「article」、「aside」、「blockquote」、「body」、「div」、「footer」、「h1…h6」、「header」、「main」、「nav」、「p」、「section」或者「span」。其他元素,比如
<img>
,不能容纳 影子树。
mode
选项可以设定封装层级。必须是以下两个值之一:
open
:任何代码都可以访问elem
的 shadow tree。通过elem.shadowRoot
访问。closed
:elem.shadowRoot
永远是null
。只能通过attachShadow
返回的指针来访问 shadow DOM(并且可能隐藏在一个 class 中)。浏览器原生的 shadow tree,比如<input type="range">
,是封闭的。没有任何方法可以访问它们。
attachShadow
返回的shadow root
和任何元素一样,可以使用innerHTML
或者其他 DOM 方法,比如append
拓展它。
🌰 例子 / <show-hello>
元素将它的内部 DOM 隐藏在了影子里面。
<script>
customElements.define('show-hello', class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<p>
Hello, ${this.getAttribute('name')}
</p>`;
}
});
</script>
<show-hello name="John"></show-hello>
2
3
4
5
6
7
8
9
10
11
12
有 shadow root 的元素叫做「shadow tree host」,可以通过 shadow root 的 host
属性访问:
console.log(elem.shadowRoot.host == elem) // true
# Shadow DOM 封装
Shadow DOM 被非常明显地和主文档分开:
- Shadow DOM 元素对于 light DOM 中的
querySelector
不可见。实际上,Shadow DOM 中的元素可能与 light DOM 中某些元素的 id 冲突。这些元素必须在 shadow tree 中独一无二。 - Shadow DOM 有自己的样式。外部样式规则在 shadow DOM 中不产生作用。
🌰 例子:
<style>
/* 文档样式对 #elem 内的 shadow tree 无作用 (1) */
p { color: red; }
</style>
<div id="elem"></div>
<script>
elem.attachShadow({mode: 'open'});
// shadow tree 有自己的样式 (2)
elem.shadowRoot.innerHTML = `
<style> p { font-weight: bold; } </style>
<p>Hello, John!</p>
`;
// <p> 只对 shadow tree 里面的查询可见 (3)
alert(document.querySelectorAll('p').length); // 0
alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到,样式对影子树没有效果,但是内部的样式有效果。
为了获取 shadow tree 内部的元素,可以从树的内部查询。
# Shadow DOM 插槽
许多类型的组件,例如标签、菜单、照片库等等,需要内容去渲染。
🌰 例子 / 像 HTML 内建的标签元素,自定义组件需要实际的内容起作用,如下的 自定义菜单组件,需要实际的标题、内容项目:
<custom-menu>
<title>Candy menu</title>
<item>Lollipop</item>
<item>Fruit Toast</item>
<item>Cup Cake</item>
</custom-menu>
2
3
4
5
6
可以尝试分析元素的内容,并动态服秩重新排列 DOM 节点。但是在 Shadow DOM 中,动态添加元素可能会丢失 CSS 样式。
Shadow DOM 支持使用 <slot>
插槽元素,由 light DOM 中的内容自动填充。
# 具名插槽
🌰 例子 :
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'})
this.shadowRoot.innerRoot = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
`;
}
});
</script>
<user-card>
<span slot="username">John Smith</span>
<span slot="birthday">2001.01.01</span>
</user-card>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
上面的例子中,从
<slot name="X">
定义了和一个 插入点,一个带有slot="X"
的元素被渲染的地方。然后浏览器执行「组合」:从 light DOM 中获取元素并且渲染到 shadow DOM 中的对象应的插槽中,最终可以获取一个能被填充数据的通用组件。
最后在浏览器编译后的 DOM 结构:
<user-card> #shadow-root <div>Name: <slot name="username"></slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card>
1
2
3
4
5
6
7
8
9
10
11现在元素同时拥有 light DOM 和 shadow DOM。为了渲染 shadow DOM 中的每一个
<slot name="...">
元素,浏览器在 light DOM 中寻找相同名字的slot="..."
:
最终结果「扁平化」后的 DOM:<user-card> #shadow-root <div>Name: <slot name="username"> <!-- slotted element is inserted into the slot --> <span slot="username">John Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"> <span slot="birthday">01.01.2001</span> </slot> </div> </user-card>
1
2
3
4
5
6
7
8
9
10
11
12
13
14扁平化 DOM(flattered DOM) 仅仅用来创建渲染和事件处理,是虚拟的。虽然是渲染出来了,但文档中的节点事实上并没有移动。如果调用
querySelectorAll
容易验证获取到的元素长度,节点仍然会在它们的位置:alert( document.querySelectorAll('user-card span').length ); // 2
1
提示
仅仅可以在 顶层子元素(直接子代) 设置
slot="..."
特性。对于嵌套的元素将被忽略。🌰 例子:
<user-card> <span slot="username">John Smith</span> <div> <!-- invalid slot, must be direct child of user-card --> <span slot="birthday">01.01.2001</span> </div> </user-card>
1
2
3
4
5
6
7
如果在 light DOM 里有多个 相同插槽名 的元素,那么它们会被一个接一个地添加到插槽中。
🌰 例子 :
<user-card>
<span slot="username">John</span>
<span slot="username">Smith</span>
</user-card>
2
3
4
结果:
<user-card>
#shadow-root
<div>Name:
<slot name="username">
<span slot="username">John</span>
<span slot="username">Smith</span>
</slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
</user-card>
2
3
4
5
6
7
8
9
10
11
12
# 插槽后备内容
在一个 <slot>
内部放点什么,它将成为后备内容。如果没有相应的填充内容,展示这个后备内容。
<div>Name:
<slot name="username">Anonymous</slot>
</div>
2
3
# 默认插槽
shadow DOM 中第一个没有名字的 <slot>
是一个默认插槽。它从 light DOM 中获取没有放置在其他位置的所有节点。
🌰 例子:
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
<fieldset>
<legend>Other information</legend>
<slot></slot>
</fieldset>
`;
}
});
</script>
<user-card>
<div>I like to swim.</div>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
<div>...And play volleyball too!</div>
</user-card>
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
所有未被插入的 light DOM 内容进入 其他信息
Other information
字段集。元素一个接一个的附加到插槽中,因此未插入插槽的信息都在默认插槽中。最终的扁平化 DOM:
<user-card> #shadow-root <div>Name: <slot name="username"> <span slot="username">John Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"> <span slot="birthday">01.01.2001</span> </slot> </div> <fieldset> <legend>About me</legend> <slot> <div>Hello</div> <div>I am John!</div> </slot> </fieldset> </user-card>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20