正文
我们来看一看浏览器利用 Shadow DOM 实现的一个示例吧,那就是 video 标签:
<video controls src="./video.mp4" width="400" height="300"></video>
复制代码
我们来看一下浏览器渲染的结果:
等一下!不是说 Shadow DOM 吗?这和普通 DOM 有啥区别???
在 Chrome 中,Elements 默认是不显示内部实现的 Shadow DOM 节点的,需要在设置中启用:
注:浏览器默认隐藏自身的 Shadow DOM 实现,但如果是用户通过脚本创造的 Shadow DOM,是不会被隐藏的。
然后,我们就可以看到 video 标签的真面目了:
在这里,你可完全像调试普通 DOM 一样随意调整 Shadow DOM 中的内容(反正和普通 DOM 一样,刷新一下就恢复了)。
我们可以看到上面这些 shadow DOM 中的节点大多都有
pseudo
属性,根据这个属性,你就可以在外面编写 CSS 样式来控制对应的节点样式了。比如,将上面这个
pseudo="-webkit-media-controls-overlay-play-button"
的 input 按钮的背景色改为橙色:
video::-webkit-media-controls-overlay-play-button {
background-color: orange;
}
复制代码
由于 Shadow DOM 实际上也是 DOM 的一种,所以在 Shadow DOM 中还可以继续嵌套 Shadow DOM,就像上面那样。
浏览器中还有很多 Element 都使用了 Shadow DOM 的形式进行封装,比如
<input>
、
<select>
、
<audio>
等,这里就不一一展示了。
由于 Shadow DOM 的隔离性,所以即便是你在外面写了个样式:
div { background-color: red !important; }
,Shadow DOM 内部的 div 也不会受到任何影响。
也就是说,写样式的时候,该用 id 的时候就用 id,该用 class 的时候就用 class,一个按钮的 class 应该写成
.button
就写成
.button
。完全不用考虑当前组件中的 id、class 可能会与其他组件冲突,你只要确保一个组件内部不冲突就好——这很容易做到。
这解决了现在绝大多数的组件化框架都面临的问题:Element 的 class(className) 到底怎么写?用前缀命名空间的形式会导致 class 名太长,像这样:
.header-nav-list-sublist-button-icon
;而使用一些 CSS-in-JS 工具,可以创造一些唯一的 class 名称,像这样:
.Nav__welcomeWrapper___lKXTg
,这样的名称仍旧有点长,还带了冗余信息。
ShadowRoot
ShadowRoot 是 Shadow DOM 下面的根,你可以把它当做 DOM 中的
<body>
一样看待,但是它不是
<body>
,所以你不能使用
<body>
上的一些属性,甚至它不是一个节点。
你可以通过 ShadowRoot 下面的
appendChild
、
querySelectorAll
之类的属性或方法去操作整个 Shadow DOM 树。
对于一个普通的 Element,比如
<div>
,你可以通过调用它上面的
attachShadow
方法来创建一个 ShadowRoot(还有一个
createShadowRoot
方法,已经过时不推荐使用),
attachShadow
接受一个对象进行初始化:
{ mode: 'open' }
,这个对象有一个
mode
属性,它有两个取值:
'open'
和
'closed'
,这个属性是在创造 ShadowRoot 的时候需要初始化提供的,并在创建 ShadowRoot 之后成为一个只读属性。
mode: 'open'
和
mode: 'closed'
有什么区别呢?在调用
attachShadow
创建 ShadowRoot 之后,
attachShdow
方法会返回 ShadowRoot 对象实例,你可以通过这个返回值去构造整个 Shadow DOM。当 mode 为
'open'
时,在用于创建 ShadowRoot 的外部普通节点(比如
<div>
)上,会有一个
shadowRoot
属性,这个属性也就是创造出来的那个 ShadowRoot,也就是说,在创建 ShadowRoot 之后,还是可以在任何地方通过这个属性再得到 ShadowRoot,继续对其进行改造;而当 mode 为
'closed'
时,你将不能再得到这个属性,这个属性会被设置为
null
,也就是说,你只能在
attachShadow
之后得到 ShadowRoot 对象,用于构造整个 Shadow DOM,一旦你失去对这个对象的引用,你就无法再对 Shadow DOM 进行改造了。
可以从上面 Shadow DOM 的截图中看到
#shadow-root (user-agent)
的字样,这就是 ShadowRoot 对象了,而括号中的
user-agent
表示这是浏览器内部实现的 Shadow DOM,如果使用通过脚本自己创建的 ShadowRoot,括号中会显示为
open
或
closed
表示 Shadow DOM 的 mode。
浏览器内部实现的
user-agent
的 mode 为
closed
,所以你不能通过节点的 ShadowRoot 属性去获得其 ShadowRoot 对象,也就意味着你不能通过脚本对这些浏览器内部实现的 Shadow DOM 进行改造。
HTML Template
有了 ShadowRoot 对象,我们可以通过代码来创建内部结构了,对于简单的结构,也许我们可以直接通过
document.createElement
来创建,但是稍微复杂一些的结构,如果全部都这样来创建不仅麻烦,而且代码可读性也很差。当然也可以通过 ES6 提供的反引号字符串(
const template = `......`;
)配合 innerHTML 来构造结构,利用反引号字符串中可以任意换行,并且 HTML 对缩进并不敏感的特性来实现模版,但是这样也是不够优雅,毕竟代码里大段大段的 HTML 字符串并不美观,即便是单独抽出一个常量文件也是一样。
这个时候就可以请 HTML Template 出场了。我们可以在 html 文档中编写 DOM 结构,然后在 ShadowRoot 中加载过来即可。
HTML Template 实际上就是在 html 中的一个
<template>
标签,正常情况下,这个标签下的内容是不会被渲染的,包括标签下的 img、style、script 等都是不会被加载或执行的。你可以在脚本中使用
getElementById
之类的方法得到
<template>
标签对应的节点,但是却无法直接访问到其内部的节点,因为默认他们只是模版,在浏览器中表现为
#document-fragment
,字面意思就是“文档片段”,可以通过节点对象的
content
属性来访问到这个 document-fragment 对象。
通过 document-fragment 对象,就可以访问到 template 内部的节点了,通过
document.importNode
方法,可以将 document-fragment 对象创建一份副本,然后可以使用一切 DOM 属性方法替换副本中的模版内容,最终将其插入到 DOM 或是 Shadow DOM 中。