声明式 Shadow DOM

我们很高兴地宣布,Safari Technology Preview 162 已添加并默认启用声明式 Shadow DOM API 支持。回顾一下,Shadow DOM 是 Web Components 的一部分,后者是由 Google 最初提出的一组规范,旨在实现在网络上创建可重用的小部件和组件。从那时起,这些规范已被整合到 DOM 和 HTML 标准中。特别是,Shadow DOM 为 DOM 树提供了轻量级封装,它允许在一个元素上创建一个名为“影子树”(shadow tree)的并行树,该树替换了元素的渲染,而无需修改其自身的 DOM 树。

在此之前,在元素上创建影子树需要在 JavaScript 中调用元素的 attachShadow() 方法。这意味着当 JavaScript 被禁用时(例如在电子邮件客户端中),此功能不可用,并且需要小心地隐藏本应在影子树中的内容,直到相关脚本加载完毕,以避免内容闪烁(flush of contents)。此外,许多现代网站和基于 Web 的应用程序采用一种称为“服务器端渲染”的技术,即在 Web 服务器上运行的程序生成带有初始内容的 HTML 标记供 Web 浏览器使用,而不是在脚本加载后通过网络获取内容。这有助于减少页面加载时间,并改善SEO,因为页面内容可以立即供搜索引擎爬虫使用。许多服务器端渲染技术试图在初始渲染时消除对 JavaScript 的需求,以减少初始绘制延迟(initial paint latency),并随着脚本和相关元数据加载而逐步增强内容的交互性。不幸的是,在使用 Shadow DOM 时,由于前面提到的需要使用 attachShadow() 的要求,这无法实现。

声明式 Shadow DOM 通过提供一种在 HTML 中包含 Shadow DOM 内容的机制来解决这些用例。具体来说,在 template 元素上指定 shadowrootmode 内容属性会告诉 Web 浏览器,此 template 元素内部的内容应放入附加到其父元素的影子树中。例如,在以下示例中,带有 shadowrootmodetemplate 元素将在 some-component 元素上附加一个影子根,该影子根包含一个文本节点“hello, world.”作为其唯一的子节点。

<some-component>
    <template shadowrootmode="closed">hello, world.</template>
</some-component>

当脚本加载并准备好使此内容具有交互性时,可以通过 ElementInternals 访问影子根,如下所示

customElements.define('some-component', class SomeComponent extends HTMLElement {
    #internals;
    constructor() {
        super();
        this.#internals = this.attachInternals();

        // This will log "hello, world."
        console.log(this.#internals.shadowRoot.textContent.trim());
    }
});

我们在设计此 API 时考虑了向后兼容性。例如,在具有声明式 Shadow DOM 的元素上调用 attachShadow() 会返回声明式附加的影子根,并删除其所有子节点,而不是通过抛出异常而失败。这意味着采用声明式 Shadow DOM 与依赖 attachShadow() 创建影子根的现有 JavaScript 兼容。请注意,默认情况下,任何 JavaScript 解析器 API(例如 DOMParserinnerHTML)都不支持声明式 Shadow DOM,以避免在接受任意模板内容的现有网站中创建新的跨站脚本漏洞(因为此类内容中的 script 元素以前是惰性的,不会运行)。

此外,我们正在引入克隆影子根的能力。在此之前,ShadowRoot 及其后代节点无法通过 cloneNode()importNode() 克隆。attachShadow() 现在接受一个 cloneable 标志作为选项。当此标志设置为 true 时,现有的 JavaScript API(例如 cloneNode()importNode())在克隆其影子宿主时也会克隆 ShadowRoot。声明式 Shadow DOM 会自动将此标志设置为 true,以便出现在其他 template 元素内部的声明式 Shadow DOM 可以与其宿主一起被克隆。在以下示例中,外部模板元素包含一个 some-component 元素实例,其影子树内容使用声明式 Shadow DOM 进行序列化。使用 document.importNode(template1.content, true) 克隆 template1.content 将克隆 some-component 及其(声明式定义的)影子树。

<template id="template1">
    <some-component>
        <template shadowrootmode="closed">hello, world.</template>
    </some-component>
</template>

总而言之,声明式 Shadow DOM 引入了一种在 HTML 中定义影子树的令人兴奋的新方式,这对于 Web Components 的服务器端渲染以及在 JavaScript 被禁用的环境(例如电子邮件客户端)中都将非常有用。这是一个备受期待的功能,浏览器供应商之间进行了大量讨论。我们很高兴地报告它已在 Safari Technology Preview 162 中引入。