<ph type="x-smartling-placeholder">
声明式 Shadow DOM 是一项标准网络平台功能,自 90 版起在 Chrome 中均受支持。请注意,此功能的规范在 2023 年发生了更改(包括将 shadowroot
重命名为 shadowrootmode
),并且在 Chrome 124 版中发布了此功能的所有部分的最新标准化版本。
浏览器支持
-
<ph type="x-smartling-placeholder">
-
<ph type="x-smartling-placeholder">
-
<ph type="x-smartling-placeholder">
-
<ph type="x-smartling-placeholder">
Shadow DOM 是三大网络组件标准之一,分为 HTML 模板和自定义元素。Shadow DOM 提供了一种将 CSS 样式的作用域限定为特定 DOM 子树,并将该子树与文档的其余部分隔离开来。<slot>
元素为我们控制了自定义元素的子项应在其阴影树中的插入位置。通过组合使用这些功能,系统可以构建自包含、可重复使用的组件,这些组件可像内置 HTML 元素一样无缝集成到现有应用中。
在此之前,使用 Shadow DOM 的唯一方法是使用 JavaScript 构建影子根:
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
这样的命令式 API 适用于客户端渲染:用于定义自定义元素的相同 JavaScript 模块也会创建其阴影根并设置其内容。不过,许多网络应用需要在构建时在服务器端呈现内容或者呈现为静态 HTML。要想为可能无法运行 JavaScript 的访问者提供合理的体验,这是非常重要的一环。
使用服务器端呈现 (SSR) 的理由因项目而异。有些网站必须提供功能完备的服务器呈现的 HTML,以符合无障碍功能指南的要求,而另一些网站则选择提供无 JavaScript 的基准体验,以确保在慢速连接或设备上获得良好性能。
过去,将 Shadow DOM 与服务器端渲染结合使用一直很困难,因为在服务器生成的 HTML 中没有内置方式来表示影子根。如果将影子根附加到已在没有影子根的情况下渲染的 DOM 元素,也会影响性能。这可能会导致页面加载后布局偏移,或者在加载 Shadow Root 的样式表时暂时显示无样式内容(“FOUC”)。
声明式 Shadow DOM (DSD) 消除了此限制,将 Shadow DOM 引入服务器。
如何构建声明式影子根
声明式阴影根是一个具有 shadowrootmode
属性的 <template>
元素:
<host-element>
<template shadowrootmode="open">
<slot></slot>
</template>
<h2>Light content</h2>
</host-element>
HTML 解析器检测到具有 shadowrootmode
属性的模板元素,并立即将其用作其父元素的影子根。从以上示例加载纯 HTML 标记会产生以下 DOM 树:
<host-element>
#shadow-root (open)
<slot>
↳
<h2>Light content</h2>
</slot>
</host-element>
此代码示例遵循 Chrome DevTools 的 Elements 面板的 Shadow DOM 内容显示规范。例如,↳
字符表示分槽的 Light DOM 内容。
这为我们提供了 Shadow DOM 在静态 HTML 中的封装和槽位投影的优势。无需 JavaScript 即可生成整个树,包括影子根。
成分水合
声明式 Shadow DOM 可单独用作封装样式或自定义子项位置的方式,但与自定义元素结合使用时效果最佳。使用自定义元素构建的组件会自动从静态 HTML 升级。随着声明式 Shadow DOM 的引入,自定义元素现在可以在升级之前拥有影子根。
从包含声明式阴影根的 HTML 升级的自定义元素已附加了该影子根。这意味着,元素在实例化时已有一个可用的 shadowRoot
属性,无需您的代码明确创建一个。最好检查 this.shadowRoot
元素的构造函数中是否存在任何现有的影子根。如果已有相应的值,则此组件的 HTML 会包含声明式阴影根。如果值为 null,说明 HTML 中不存在声明式阴影根,或者浏览器不支持声明式阴影 DOM。
<menu-toggle>
<template shadowrootmode="open">
<button>
<slot></slot>
</button>
</template>
Open Menu
</menu-toggle>
<script>
class MenuToggle extends HTMLElement {
constructor() {
super();
// Detect whether we have SSR content already:
if (this.shadowRoot) {
// A Declarative Shadow Root exists!
// wire up event listeners, references, etc.:
const button = this.shadowRoot.firstElementChild;
button.addEventListener('click', toggle);
} else {
// A Declarative Shadow Root doesn't exist.
// Create a new shadow root and populate it:
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<button><slot></slot></button>`;
shadow.firstChild.addEventListener('click', toggle);
}
}
}
customElements.define('menu-toggle', MenuToggle);
</script>
自定义元素已经存在了一段时间,到目前为止,在使用 attachShadow()
创建影子根之前,没有理由检查是否存在现有的影子根。声明式 Shadow DOM 做出了一项细微更改,使现有组件仍可正常工作:对具有现有声明式阴影根的元素调用 attachShadow()
方法不会抛出错误。而是会清空并返回声明式影子根。这样,未针对声明式 Shadow DOM 构建的旧组件可以继续工作,因为声明式根会保留到命令式替换创建之前。
对于新创建的自定义元素,新的 ElementInternals.shadowRoot
属性提供了一种明确的方法来引用元素的现有声明式阴影根,包括打开和关闭。这可用于检查和使用任何声明式影子根,同时在未提供声明式影子根的情况下仍会回退到 attachShadow()
。
class MenuToggle extends HTMLElement {
constructor() {
super();
const internals = this.attachInternals();
// check for a Declarative Shadow Root:
let shadow = internals.shadowRoot;
if (!shadow) {
// there wasn't one. create a new Shadow Root:
shadow = this.attachShadow({
mode: 'open'
});
shadow.innerHTML = `<button><slot></slot></button>`;
}
// in either case, wire up our event listener:
shadow.firstChild.addEventListener('click', toggle);
}
}
customElements.define('menu-toggle', MenuToggle);
每个根一个阴影
声明式阴影根仅与其父元素关联。这意味着影子根始终与其关联的元素共存。这一设计决策可确保影子根像 HTML 文档的其余部分一样可流式传输。这还便于编写和生成,因为向元素添加影子根不需要维护现有影子根的注册表。
将影子根与其父元素相关联的权衡在于,系统无法从一个声明式阴影根 <template>
对多个元素进行初始化。不过,在使用声明式 Shadow DOM 的大多数情况下,这不太可能发生问题,因为每个影子根的内容几乎完全相同。虽然服务器呈现的 HTML 通常包含重复的元素结构,但其内容通常会有所差异,例如文字或属性会略有不同。由于序列化的声明式影子根的内容是完全静态的,因此仅当这些元素恰巧相同时,才能从单个声明式影子根升级多个元素。最后,由于压缩的影响,重复的类似影子根对网络传输大小的影响相对较小。
将来,或许可以重新访问共享的影子根。如果 DOM 获得对内置模板的支持,则可以将声明式阴影根视为实例化的模板,以便为指定元素构建影子根。当前的声明式 Shadow DOM 设计允许将影子根关联限制为单个元素,从而在未来实现这种可能性。
在线播放功能很酷
直接将声明式影子根与其父元素相关联可简化升级并将其附加到该元素的流程。声明式影子根会在 HTML 解析过程中检测出来,并在遇到其起始 <template>
标记时立即附加。<template>
中解析后的 HTML 会直接解析到影子根中,因此可以“流式传输”:在接收时进行渲染。
<div id="el">
<script>
el.shadowRoot; // null
</script>
<template shadowrootmode="open">
<!-- shadow realm -->
</template>
<script>
el.shadowRoot; // ShadowRoot
</script>
</div>
仅限解析器
声明式 Shadow DOM 是 HTML 解析器的一项功能。这意味着,系统只会为 HTML 解析期间存在的具有 shadowrootmode
属性的 <template>
标记解析和附加声明式影子根目录。换言之,声明式影子根可以在初始 HTML 解析期间构建:
<some-element>
<template shadowrootmode="open">
shadow root content for some-element
</template>
</some-element>
设置 <template>
元素的 shadowrootmode
属性不会执行任何操作,并且模板仍然是普通的模板元素:
const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null
为避免一些重要的安全注意事项,不能使用 innerHTML
或 insertAdjacentHTML()
等 fragment 解析 API 创建声明式影子根。解析应用了声明式阴影根的 HTML 的唯一方法是使用 setHTMLUnsafe()
或 parseHTMLUnsafe()
:
<script>
const html = `
<div>
<template shadowrootmode="open"></template>
</div>
`;
const div = document.createElement('div');
div.innerHTML = html; // No shadow root here
div.setHTMLUnsafe(html); // Shadow roots included
const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>
具有样式的服务器渲染
使用标准的 <style>
和 <link>
标记,声明式影子根完全支持内嵌和外部样式表:
<nineties-button>
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<link rel="stylesheet" href="/comicsans.css" />
<button>
<slot></slot>
</button>
</template>
I'm Blue
</nineties-button>
以这种方式指定的样式也经过高度优化:如果同一个样式表存在于多个声明式阴影根中,则它只会加载和解析一次。浏览器使用由所有影子根共享的单个后备 CSSStyleSheet
,从而消除了重复的内存开销。
声明式 Shadow DOM 不支持可构造的样式表。这是因为,目前无法在 HTML 中对可构造的样式表进行序列化,也无法在填充 adoptedStyleSheets
时引用这些样式表。
如何避免闪烁无样式的内容
在尚不支持声明式 Shadow DOM 的浏览器中,有一个潜在问题是避免“闪烁无样式的内容”(FOUC):针对尚未升级的自定义元素显示原始内容。在声明式 Shadow DOM 出现之前,避免 FOUC 的一种常用方法是对尚未加载的自定义元素应用 display:none
样式规则,因为这些元素尚未附加和填充影子根。采用这种方式,内容在“ready”后才会显示:
<style>
x-foo:not(:defined) > * {
display: none;
}
</style>
随着声明式 Shadow DOM 的引入,自定义元素可以在 HTML 中渲染或编写,使得其阴影内容就位且在客户端组件实现加载之前准备就绪:
<x-foo>
<template shadowrootmode="open">
<style>h2 { color: blue; }</style>
<h2>shadow content</h2>
</template>
</x-foo>
在本示例中,display:none
“FOUC”规则将阻止声明性影子根的内容显示。但是,移除该规则会导致不支持声明式 Shadow DOM 的浏览器显示不正确或无样式的内容,直到声明式 Shadow DOM polyfill 加载并将影子根模板转换为真正的影子根之前。
幸运的是,这可以通过修改 FOUC 样式规则在 CSS 中解决。在支持声明式 Shadow DOM 的浏览器中,<template shadowrootmode>
元素会立即转换为影子根,不会在 DOM 树中留下任何 <template>
元素。不支持声明式 Shadow DOM 的浏览器会保留 <template>
元素,因此我们可以使用该元素来防止 FOUC:
<style>
x-foo:not(:defined) > template[shadowrootmode] ~ * {
display: none;
}
</style>
我们并没有隐藏尚未定义的自定义元素,而是修改了“FOUC”规则会隐藏其子项,前提是子项跟随 <template shadowrootmode>
元素。自定义元素一经定义,规则便不再匹配。在支持声明式 Shadow DOM 的浏览器中,该规则会被忽略,因为 <template shadowrootmode>
子项在 HTML 解析过程中会被移除。
功能检测和浏览器支持
声明式 Shadow DOM 自 Chrome 90 和 Edge 91 起便已提供,但它使用的是名为 shadowroot
的旧版非标准属性,而不是标准化的 shadowrootmode
属性。Chrome 111 和 Edge 111 提供了较新的 shadowrootmode
属性和流式传输行为。
作为一种新的网络平台 API,声明式 Shadow DOM 尚未在所有浏览器上获得广泛的支持。可以通过检查 HTMLTemplateElement
原型上是否存在 shadowRootMode
属性来检测浏览器支持:
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
聚酯纤维
为声明式 Shadow DOM 构建简化的 polyfill 相对来说比较简单,因为 polyfill 不需要完美复制浏览器实现涉及到的时间语义或仅解析器的特性。如需对声明式 Shadow DOM 执行 polyfill 操作,我们可以扫描 DOM 以查找所有 <template shadowrootmode>
元素,然后将其转换为在其父元素上附加的阴影根。此过程可以在文档准备就绪后完成,也可以由更具体的事件(例如自定义元素生命周期)触发。
(function attachShadowRoots(root) {
root.querySelectorAll("template[shadowrootmode]").forEach(template => {
const mode = template.getAttribute("shadowrootmode");
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
attachShadowRoots(shadowRoot);
});
})(document);