Shadow DOM 用于封装。它允许组件拥有自己的“影子”DOM 树,该树无法从主文档中意外访问,可能具有本地样式规则,等等。
内置 Shadow DOM
你有没有想过浏览器控件是如何创建和设置样式的?
例如 <input type="range">
浏览器在内部使用 DOM/CSS 来绘制它们。该 DOM 结构通常对我们隐藏,但我们可以在开发者工具中看到它。例如,在 Chrome 中,我们需要在开发者工具中启用“显示用户代理 Shadow DOM”选项。
然后 <input type="range">
看起来像这样
你在 #shadow-root
下看到的是“Shadow DOM”。
我们无法通过常规的 JavaScript 调用或选择器获取内置的 Shadow DOM 元素。这些不是常规的子元素,而是一种强大的封装技术。
在上面的例子中,我们可以看到一个有用的属性pseudo
。它是非标准的,出于历史原因存在。我们可以用它来用 CSS 样式化子元素,就像这样
<style>
/* make the slider track red */
input::-webkit-slider-runnable-track {
background: red;
}
</style>
<input type="range">
再次强调,pseudo
是一个非标准属性。从时间顺序上来说,浏览器首先开始尝试使用内部 DOM 结构来实现控件,然后,随着时间的推移,影子 DOM 被标准化,允许我们开发者做类似的事情。
接下来,我们将使用现代影子 DOM 标准,该标准由DOM 规范和其他相关规范涵盖。
影子树
一个 DOM 元素可以有两个类型的 DOM 子树
- 光树 - 一个普通的 DOM 子树,由 HTML 子元素组成。我们在前面章节中看到的所有子树都是“光”树。
- 影子树 - 一个隐藏的 DOM 子树,没有反映在 HTML 中,对窥视的眼睛隐藏。
如果一个元素同时拥有两者,那么浏览器只渲染影子树。但是我们也可以在影子树和光树之间建立一种组合。我们将在本章的后面部分看到影子 DOM 插槽,组合的详细信息。
影子树可以在自定义元素中使用,以隐藏组件内部结构并应用组件本地样式。
例如,这个<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>
这就是 Chrome 开发工具中生成的 DOM 的样子,所有内容都在“#shadow-root”下
首先,调用elem.attachShadow({mode: …})
创建一个影子树。
有两个限制
- 我们只能为每个元素创建一个影子根。
elem
必须是自定义元素,或者以下元素之一:“article”、“aside”、“blockquote”、“body”、“div”、“footer”、“h1…h6”、“header”、“main” “nav”、“p”、“section”或“span”。其他元素,例如<img>
,不能承载影子树。
mode
选项设置封装级别。它必须具有以下两个值之一
-
"open"
- 影子根可作为elem.shadowRoot
使用。任何代码都可以访问
elem
的影子树。 -
"closed"
-elem.shadowRoot
始终为null
。我们只能通过
attachShadow
返回的引用(可能隐藏在一个类中)来访问影子 DOM。浏览器原生影子树,例如<input type="range">
,是封闭的。没有办法访问它们。
由attachShadow
返回的影子根就像一个元素:我们可以使用innerHTML
或 DOM 方法(例如append
)来填充它。
具有影子根的元素称为“影子树宿主”,它作为影子根的host
属性可用
// assuming {mode: "open"}, otherwise elem.shadowRoot is null
alert(elem.shadowRoot.host === elem); // true
封装
Shadow DOM 与主文档严格隔离
- Shadow DOM 元素对来自 light DOM 的
querySelector
不可見。特别是,Shadow DOM 元素可能具有与 light DOM 中的元素冲突的 id。它们必须在 shadow tree 内唯一。 - Shadow DOM 拥有自己的样式表。来自外部 DOM 的样式规则不会应用。
例如
<style>
/* document style won't apply to the shadow tree inside #elem (1) */
p { color: red; }
</style>
<div id="elem"></div>
<script>
elem.attachShadow({mode: 'open'});
// shadow tree has its own style (2)
elem.shadowRoot.innerHTML = `
<style> p { font-weight: bold; } </style>
<p>Hello, John!</p>
`;
// <p> is only visible from queries inside the shadow tree (3)
alert(document.querySelectorAll('p').length); // 0
alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
- 来自文档的样式不会影响 shadow tree。
- …但来自内部的样式有效。
- 要获取 shadow tree 中的元素,我们必须从 tree 内部进行查询。
参考资料
- DOM: https://dom.spec.whatwg.org/#shadow-trees
- 兼容性: https://caniuse.cn/#feat=shadowdomv1
- Shadow DOM 在许多其他规范中都有提及,例如 DOM 解析 指定 shadow root 具有
innerHTML
。
总结
Shadow DOM 是一种创建组件本地 DOM 的方法。
shadowRoot = elem.attachShadow({mode: open|closed})
– 为elem
创建 Shadow DOM。如果mode="open"
,则它可以通过elem.shadowRoot
属性访问。- 我们可以使用
innerHTML
或其他 DOM 方法填充shadowRoot
。
Shadow DOM 元素
- 拥有自己的 id 空间,
- 对来自主文档的 JavaScript 选择器(如
querySelector
)不可见, - 仅使用来自 shadow tree 的样式,而不是来自主文档的样式。
如果存在,Shadow DOM 将由浏览器渲染,而不是所谓的“light DOM”(普通子元素)。在章节 Shadow DOM 插槽,组合 中,我们将看到如何组合它们。
评论
<code>
标签,对于多行代码,请将它们包装在<pre>
标签中,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)