我害怕所有那些在活着时不曾满足的欲望和未及消耗的精力会在我死后继续折磨着我。我希望能在世间充分表达自己内心的所有渴望,然后心满意足,了无希望地死去。 —— 安德烈·纪德《人间食粮》
Shadow DOM 给我们提供了封装的功能。允许一个组件拥有自己的 Shadow DOM 树,这个树不会被主文档查询方法意外访问到,而且还可以声明组件级别的本地样式等。
内置 shadow DOM
你有想过浏览器给我们提供的原生控件,也是需要给样式、定义行为的,而且还是还是有点复杂的。
下面是一个滑块控件 <input type="range">
,看起来是这样子的:
浏览器会使用内部的 DOM/CSS 来绘制滑块控件。DOM 结构对我们通常是不可见的,但我们可以在开发者工具中检查到。在 Chrome 中,我们需要启用开发者工具的“Show user agent shadow DOM”选项,就能看到这些隐藏的结构。
启用上述选项后,检查 <input type="range">
元素我们会看到这样的结构:
#shadow-root
之下的部分称之为“Shadow DOM”。
我们无法通过常规的 JavaScript 调用和选择器获得这些内置的 shadow DOM 元素。因为它们不是常规的子元素,只是应用了一种强大的封装手法。
上例中,我们看到了一个 pseudo
属性。这是一个未被规范化、因历史原因保留的属性,我们可以借助它用 CSS 给子元素加样式。
<style>
/* 让滑块轨道呈现红色 */
input::-webkit-slider-runnable-track {
background: red;
}
</style>
<input type="range">
再啰嗦一下,pseudo
是一个未被标准化的属性。因为在早期,浏览器厂商为了能够控制内部 DOM 结构(internal DOM structures)而引入的一个实验特性,之后为了兼容而得到保留。Shadow DOM 标准出台后,允许我们用其他方式,实现同样的功能。
接下来,我们将使用 DOM及其他相关规范中定义的 shadow DOM 标准讲解。
Shadow 树
一个 DOM 元素可以拥有两类 DOM 子树:
- Light 树:常规的 DOM 子树,由 HTML 孩子元素组成。在接触 Shadow 树之前,我们碰到的都是树都是“Light”的。
- Shadow 树:隐藏的 DOM 子树,没有反映在 HTML 中,对我们来说是透明的。
如果一个元素同时拥有这两棵子树,浏览器只会渲染 shadow 树。不过我们也可以同时创建一个由 shadow 和 light 树组成的元素,这会在后面的章节《Shadow DOM slots, composition》中介绍。
Shadow 树可以用于自定义元素——封装内部实现,使用组件级别的本地样式。
例如,下面的 <show-hello>
就将内部 DOM 结构隐藏在 Shadow 树中了。
<script>
customElements.define('show-hello', class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `<p>Hello, ${this.getAttribute('name')}</p>`
}
})
</script>
使用 Chrome 检查自定义元素 <show-hello>
的 DOM 结构,所有的内容现在都在 #shadow-root
之下:
首先,我们使用了 elem.attachShadow({ mode: 'open' })
创建了一个 Shadow 树。
这里有两个限制:
- 每个元素只能创建一棵 Shadow 树,也就是说只有一个 shadow root。
elem
必须是一个自定义元素,或者是下列元素之一:“article”、“aside”、“blockquote”、“body”、“div”、“footer”、“h1…h6”、“header”、“main”、“nav”、“p”、“section” 和 “span”。其他的元素,比如<img>
是不能创建 shadow 树的,也就是说不能作为 shadow 树的宿主。
mode
选项设定封装级别,取值为下列两个之一:
-
'open'
:可以使用elem.shadowRoot
获取 shadow root。此种情况下,我们就可以显式操作
elem
的 Shadow 树了。 -
'close'
:与'open'
相反,访问elem.shadowRoot
得到的是null
。此种情况下,我们唯一能访问 Shadow 树的地方,就是通过引用
attachShadow
方法的返回值。浏览器原生的 shadow 树,比方说<input type="range">
,就是 closed,就没有办法访问了。
attachShadow
方法返回的 shadow root 类似一个元素:我们可以对它使用 innerHTML 或 DOM 方法,比如 append
,来填充元素。
具有 shadow root 的元素称为“shadow 树宿主”,可以通过 shadow root 的 host
属性访问到:
// 假设 {mode: "open"} 否则访问 elem.shadowRoot 得到的是 null
elem.shadowRoot.host === elem // true
封装
Shadow DOM 与主文档(main document)是严格分离的:
- Shadow DOM 元素不能使用 light DOM 的 querySelector 方法访问到。而且,Shadow DOM 元素的 id 可以与主文档里的某个元素的一样,只要保证在 shadow 树里是唯一的就可以了。
- Shadow DOM 有自己的样式,外部的样式规则不会影响它。
例如:
<style>
/* 此处的文档样式不会影响 #helloElem 中 shadow 树 (1) */
p {
color: red;
}
</style>
<show-hello id="helloElem" name="World"></show-hello>
<script>
customElements.define('show-hello', class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
// shadow 树有自己的样式 (2)
shadow.innerHTML = `
<style>
p {
font-weight: bold;
}
</style>
<p>Hello, ${this.getAttribute('name')}</p>
`
}
})
// <p> 元素是 shadow 树中的,所以要在 shadow 树范围内才能找到 (3)
console.log(document.querySelectorAll('p').length) // 0
console.log(helloElem.shadowRoot.querySelectorAll('p').length) // 1
</script>
- 文档样式不会影响 shadow 树的,
- shadow 树中定义的样式才对 shadow 树的样式起作用,
- shadow 树中的元素,在文档中是查询不到的。
参考链接
- DOM:dom.spec.whatwg.org/#shadow-tre…
- 兼容性:caniuse.com/#feat=shado…
- Shadow DOM 还在其他规范中提到,比如 DOM Parsing 定义了 shadow root 上存在
innerHTML
属性
总结
Shadow DOM 是创建组件级 DOM(component-local DOM)的一种方式。
shadowRoot = elem.attachShadow({mode: open|closed})
:为元素elem
创建 shadow DOM。如果mode='open'
的话,我们可以通过elem.shadowRoot
属性访问到 shadow DOM。- 我们可以使用
innerHTML
或其他 DOM 方法填充shadowRoot
。
Shadow DOM 元素:
- 有自己的 id 空间域,
- 对主文档 JavaScript 选择器
querySelector*
方法是不可见的, - 只会应用 shadow DOM 内部声明的样式,主文档样式对其不起作用。
Shadow DOM 是由浏览器渲染、而非由“light DOM”(常规元素)渲染的。在《Shadow DOM slots, composition》 会介绍怎么组合常规元素和 Shadow DOM 元素。
(完)