目录

🍘 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 访问。
  • closedelem.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>
1
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
1

# Shadow DOM 封装

Shadow DOM 被非常明显地和主文档分开

  1. Shadow DOM 元素对于 light DOM 中的 querySelector 不可见。实际上,Shadow DOM 中的元素可能与 light DOM 中某些元素的 id 冲突。这些元素必须在 shadow tree 中独一无二。
  2. 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>
1
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>
1
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>
1
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>
1
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>
1
2
3
4
5
6
7
8
9
10
11
12

# 插槽后备内容

在一个 <slot> 内部放点什么,它将成为后备内容。如果没有相应的填充内容,展示这个后备内容。

<div>Name:
  <slot name="username">Anonymous</slot>
</div>
1
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>
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

所有未被插入的 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

# 更新插槽

# 插槽 API

# Shadow DOM 样式

# :host

# 级联

# Shadow DOM 的事件

📢 上次更新: 2022/09/02, 10:18:16