Shadow DOM v1 - 独立的 Web 组件

Shadow DOM 可让网络开发者为网络组件创建分隔的 DOM 和 CSS

摘要

Shadow DOM 解决了构建 Web 应用的脆弱性问题。脆弱性 适用于 HTML、CSS 和 JS 的全局性多年来, 发明了一个超高的数字 / 工具 来规避这些问题。例如,当您使用新的 HTML ID/类时, 无法判断该名称是否与网页使用的现有名称冲突。 小虫子就会爬到 CSS 的特异性成为一个大问题(!important 一切皆可!),样式 选择器会失去控制, 性能可能会受到影响。列表 继续运行

Shadow DOM 修复了 CSS 和 DOM。它在 Web 中引入了作用域样式 平台。无需工具或命名惯例,您可以将将 CSS 与 标记、隐藏实现详情以及作者独立 组件

简介

Shadow DOM 是三种网络组件标准之一: HTML 模板Shadow DOM自定义元素HTML 导入 以前包含在列表中,但现在被视为 已弃用

您无需编写使用 shadow DOM 的网络组件。但当您这样做时 您可以利用其优势(CSS 作用域、DOM 封装、 组合)和构建可重复使用 自定义元素 具有出色的弹性、可配置性和可重用性。如果为自定义 元素是创建新 HTML 的方式(通过 JS API),shadow DOM 则是 您提供 HTML 和 CSS 的方式。这两个 API 组合起来构成一个组件 使用独立的 HTML、CSS 和 JavaScript 编写代码。

Shadow DOM 是一款用于构建基于组件的应用的工具。因此, 它为 Web 开发中的常见问题提供了解决方案:

  • 隔离 DOM:组件的 DOM 是独立的(例如 document.querySelector() 不会返回组件 shadow DOM 中的节点)。
  • 作用域 CSS:在 shadow DOM 内部定义的 CSS 的作用域限定为作用域。样式规则 不会泄漏,页面样式也不会渗入。
  • 组合:为组件设计一个基于标记的声明性 API。
  • 简化 CSS - 作用域 DOM 意味着您可以使用简单的 CSS 选择器, 通用 ID/类名称,无需担心命名冲突。
  • 效率 - 将应用看成是多个 DOM 块,而不是一个大型 (全局)页面。

fancy-tabs 演示

在整篇文章中,我都会引用一个演示组件 (<fancy-tabs>) 并引用其中的代码段如果您的浏览器支持 API, 应该可以在下面看到现场演示否则,请查看 GitHub 上的完整源代码

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
<ph type="x-smartling-placeholder"></ph> 在 GitHub 上查看源代码

什么是 shadow DOM?

DOM 相关背景

HTML 因其易于使用的特点驱动着网络的发展。通过声明几个标记, 只需几秒的时间,即可创作出一个既有演示文稿又有条理的页面。不过, 但 HTML 本身并不实用用户很容易理解文本 但机器需要更多东西。输入 Document 对象 模型,即 DOM。

浏览器在加载网页时会执行一些有趣的操作。以下之一: 它的作用就是将作者的 HTML 转换为实时文档。 从根本上说,为理解网页的结构,浏览器会解析 HTML(静态 文本字符串)转换为数据模型(对象/节点)。浏览器会保留 通过创建由这些节点组成的树来确定 HTML 的层次结构:DOM。超酷的方面 DOM 的另一个特点是,它可以实时呈现您的网页与静态 我们编写的 HTML、浏览器生成的节点包含属性、方法和最佳 都可能会被程序操纵!这就是我们能够创建 DOM 的原因所在 元素:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

会生成以下 HTML 标记:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

一切正常。然后 到底什么是 shadow DOM

阴影中的 DOM

Shadow DOM 与普通 DOM 相同,但有两点区别:1) 其创建/使用方式; 2) 相对于网页其他部分的表现方式。通常,创建 DOM 时 节点,并将其作为另一个元素的子元素附加。利用 shadow DOM,您可以 创建一个作用域 DOM 树,该 DOM 树附加到该元素上,但与其 真实的孩子。这种限定了范围的子树称为影子树。元素 是它被挂接到的影子主机。您在阴影中添加的任何内容都会变为 本地元素,包括 <style>。这就是 shadow DOM 可实现 CSS 样式作用域。

创建 shadow DOM

影子根是附加到“宿主”元素的文档 fragment。 元素通过附加影子根来获取其 shadow DOM。接收者 为元素创建 shadow DOM,调用 element.attachShadow()

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

我使用 .innerHTML 来填充影子根,但您也可以使用其他 DOM API。这就是网络。我们有选择。

该规范定义了元素列表 无法托管影子树导致某个元素 列表中:

  • 浏览器已为该元素托管自己的内部 shadow DOM (<textarea><input>)。
  • 让元素托管 shadow DOM 毫无意义 (<img>)。

例如,以下代码行不通:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

为自定义元素创建 shadow DOM

创建阴影时,阴影 DOM 特别有用。 自定义元素。 使用 shadow DOM 来分隔元素的 HTML、CSS 和 JS,从而 从而生成“网络组件”

示例 - 自定义元素将 shadow DOM 附加到自身, 封装其 DOM/CSS:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

这里有几个有趣的事情。第一个是 当 <fancy-tabs> 的实例时,自定义元素会创建自己的 shadow DOM 。这在 constructor() 中完成。其次,因为我们打造了 影子根,则 <style> 内的 CSS 规则的范围将限定为 <fancy-tabs>

组合和槽

组合是 shadow DOM 最难理解的功能之一, 无疑是最重要的

在网络开发世界中,组合是指我们构建应用的方式, 将其排除在 HTML 之外。不同的构建块(<div><header><form><input>)组合在一起构成应用。有些代码甚至能正常运行 相互通信。组合是 <select> 等原生元素的原因, <details><form><video> 非常灵活。这些代码中的每个代码都接受 并将其作为子项进行特殊处理例如: <select> 知道如何将 <option><optgroup> 呈现为下拉菜单, 支持多选微件<details> 元素将 <summary> 渲染为 展开箭头。就连<video>也知道如何与某些孩子相处: <source> 元素不会渲染,但它们会影响视频的行为。 多么神奇!

术语:light DOM 与 shadow DOM

Shadow DOM 组合引入了大量 Web 的新基础知识 开发。我们先来标准化一些 所以我们说的都是同一个术语。

轻量级 DOM

组件用户编写的标记。此 DOM 不在 组件的 shadow DOM。它是元素的实际子项。

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

阴影 DOM

DOM 是由组件作者编写的。Shadow DOM 对于组件而言是本地的,并且 定义其内部结构、作用域 CSS,并封装您的实现 。它还可以定义如何呈现由使用者编写的标记 组件的专属属性。

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

扁平的 DOM 树

浏览器将用户的 light DOM 分布到您的阴影中的结果 DOM,渲染最终产品。扁平的树是您最终看到的 以及页面上呈现的内容。

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

<slot>元素

Shadow DOM 使用 <slot> 元素将不同的 DOM 树组合在一起。 槽是组件内的占位符,用户可以使用 自己的标记。通过定义一个或多个广告位,您可以邀请外部标记来呈现 。从本质上讲,您的意思是“渲染用户的 标记”

元素可以“交叉”当 <slot> 发出邀请时,影子 DOM 边界 。这些元素称为“分布式节点”。从概念上讲, 分布式节点似乎有点奇怪。Slot 实际上不会移动 DOM;他们 在 shadow DOM 内的其他位置进行渲染。

组件可以在其 shadow DOM 中定义零个或多个 slot。槽可以为空 或提供后备内容。如果用户未提供 light DOM 则广告位会呈现其后备内容。

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

您还可以创建已命名的槽。已命名的广告位是指 用户通过名称引用的 shadow DOM。

示例 - <fancy-tabs> 的 shadow DOM 中的槽位:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

组件用户声明 <fancy-tabs> 的方式如下所示:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

而且,如果您很好奇,扁平的树看起来如下所示:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

请注意,我们的组件能够处理不同的配置,但是 扁平化 DOM 树保持不变。我们还可以从<button>切换到 <h2>。编写此组件的目的在于处理不同类型的子项... 就像<select>所做的那样!

样式

您可以通过多种方式设置网络组件的样式。使用阴影的组件 DOM 可通过主页面设置样式、定义其自己的样式或提供钩子(在 CSS 自定义属性的形式)供用户替换默认设置。

组件定义的样式

请记住,shadow DOM 最有用的功能是作用域 CSS

  • 外部页面中的 CSS 选择器不适用于组件。
  • 内部定义的样式不会溢出。它们的作用域限定为主元素。

shadow DOM 内部使用的 CSS 选择器在本地应用于组件。在 这意味着我们可以再次使用常见的 ID/类名称,无需担心 网页上是否有冲突最佳做法是采用更简单的 CSS 选择器 位于 Shadow DOM 内。它们对性能也很不错。

示例 - 在影子根中定义的样式是本地样式

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

样式表的范围也限定为影子树:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

有没有想过,<select> 元素如何渲染多选 widget(而不是 下拉菜单),添加 multiple 属性即可:

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select> 能够根据您选择的属性为自身设置不同的样式 对该文件进行声明网络组件也可以使用 :host 自行设置样式 选择器。

示例 - 组件对自身进行样式设置

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

:host 的一个问题是父页中的规则具有更高的特异性 高于元素中定义的 :host 条规则。也就是说,外部样式胜出。这个 允许用户从外部覆盖您的顶级样式。此外,:host 仅适用于影子根,因此不能在非影子根之外使用 shadow DOM。

:host(<selector>) 的函数形式允许您定位主机(如果 与 <selector> 匹配。这种封装方式非常适合 对用户互动或状态做出反应的行为,或根据内部节点的样式 主机上。

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

根据上下文设置样式

如果组件或其任何祖先实体,则 :host-context(<selector>) 与该组件匹配 与 <selector> 匹配。一种常见用途是基于组件的 环境。例如,许多人通过将一个类应用于 <html><body>

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

<fancy-tabs> 是后代时,:host-context(.darktheme) 将设置其样式 (共 .darktheme 个):

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() 对于设置主题很有用,但更好的方法是 使用 CSS 自定义属性创建样式钩子

设置分布式节点的样式

::slotted(<compound-selector>) 匹配分布到 <slot>

假设我们已创建了一个名称徽章组件:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

组件的 shadow DOM 可为用户的 <h2>.title 设置样式:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

如果您还记得之前的内容,<slot> 不会移动用户的 light DOM。时间 节点分布到 <slot> 中,<slot> 会渲染其 DOM,但 物理节点留在原处分布前已应用的样式会继续 应用。不过,light DOM 分布后,它就可以 可以采用其他样式(由 shadow DOM 定义的样式)。

另一个来自 <fancy-tabs> 的更深入的示例:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

在本例中,有两个 slot:一个用于标签页标题的命名 slot,以及 广告位。当用户选择某个标签页时,我们会加粗其选择 并显示其面板这是通过选择 selected 属性。自定义元素的 JS(此处未显示)添加了 属性。

从外部设置组件样式

从外部为组件设置样式的方法有多种。最简单 方法是使用标记名称作为选择器:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

外部样式始终优先于在 shadow DOM 中定义的样式。例如: 如果用户编写选择器 fancy-tabs { width: 500px; },它将优先于 组件规则::host { width: 650px;}

设置组件本身的样式只能到此为止。但是如果你 想要设置组件内部样式?为此,我们需要使用 属性。

使用 CSS 自定义属性创建样式钩子

如果组件的作者提供了样式钩子,用户就可以调整内部样式 使用 CSS 自定义属性进行设置。从概念上讲,这类似于 <slot>。您创建“样式占位符”供用户覆盖

示例 - <fancy-tabs> 允许用户替换背景颜色:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

在其 shadow DOM 内部:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

在本例中,该组件将使用 black 作为背景值,因为 是由用户提供的。否则,将默认为 #9E9E9E

高级主题

创建闭合影子根(应避免)

shadow DOM 的另一种形式称为“闭合”模式。创建 闭合影子树,在 JavaScript 外部无法访问内部 DOM 组件的专属属性。这与 <video> 等原生元素的工作原理类似。 JavaScript 无法访问 <video> 的 shadow DOM,因为浏览器 则使用闭合模式影子根来实现。

示例 - 创建一个闭合影子树:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

其他 API 也会受到闭合模式的影响:

  • Element.assignedSlot / TextNode.assignedSlot 返回 null
  • Event.composedPath(),适用于与阴影内的元素关联的事件 DOM,返回 []

下面我总结一下,切勿使用 {mode: 'closed'}:

  1. 人为的安全感。没有什么能够阻止攻击者 盗用 Element.prototype.attachShadow

  2. 闭合模式会阻止自定义元素代码访问其自己的 shadow DOM。那可是完全不对的。您必须存储一个引用 供您稍后使用。querySelector()这完全 违背了闭合模式的最初用途!

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. 闭合模式会降低组件对最终用户的灵活性。您 但有时您可能会忘记添加 功能。配置选项。用户想要的用例。常见 例如忘记为内部节点添加足够的样式钩子。 在闭合模式下,用户无法覆盖默认设置并进行调整 样式。能够访问组件的内部内容非常有帮助。 最终,用户会复刻您的组件、寻找其他组件或创建自己的组件 自己就会成为客户的 :(

在 JS 中使用槽

shadow DOM API 提供了与 slot 和分布式 节点。在编写自定义元素时,这些方法会派上用场。

slotchange 事件

当槽的分布式节点发生变化时,会触发 slotchange 事件。对于 例如,当用户从 light DOM 中添加/移除子项时。

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

要监控 Light DOM 的其他类型的更改,您可以设置 MutationObserver

哪些元素正在广告位中呈现?

有时,了解哪些元素与槽位相关联很有用。致电 slot.assignedNodes(),用于查找槽正在呈现哪些元素。通过 {flatten: true} 选项还会返回广告位的后备内容(如果没有节点 )。

举个例子,假设您的 shadow DOM 如下所示:

<slot><b>fallback content</b></slot>
用法致电结果
<my-component>组件文字</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

元素分配到哪个位置?

也可以回答反向问题。element.assignedSlot 会告知 该元素会分配到哪个组件槽位。

Shadow DOM 事件模型

当事件从 shadow DOM 中触发时,其目标会进行调整,以维持 封装。也就是说,系统会重新定位事件 好像它们来自组件,而不是内部元素 shadow DOM。有些事件甚至不会从 shadow DOM 中传播出去。

确实会跨越影子边界的事件包括:

  • 焦点事件:blurfocusfocusinfocusout
  • 鼠标事件:clickdblclickmousedownmouseentermousemove
  • 滚轮事件:wheel
  • 输入事件:beforeinputinput
  • 键盘事件:keydownkeyup
  • 组合事件:compositionstartcompositionupdatecompositionend
  • DragEvent:dragstartdragdragenddrop

提示

如果影子树处于打开状态,调用 event.composedPath() 将返回数组 事件经过的节点数量

使用自定义事件

在影子树中的内部节点上触发的自定义 DOM 事件不会 除非事件是使用 composed: true 标志:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

如果为 composed: false(默认值),消费者将无法监听事件 在影子根外部运行

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

处理焦点

如果您从 shadow DOM 的事件模型中调用,则被触发的事件 对 shadow DOM 内部进行调整,使其看起来像来自托管元素。 例如,假设您点击影子根内的 <input>

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

focus 事件将显示为来自 <x-focus>,而不是 <input>。 同样,document.activeElement 将为 <x-focus>。如果影子根 是使用 mode:'open' 创建的(请参阅闭合模式),您还将 可以访问获得焦点的内部节点:

document.activeElement.shadowRoot.activeElement // only works with open mode.

如果有多个级别的 shadow DOM 调用(例如,某个自定义元素在 另一个自定义元素),您需要以递归方式深入影子根, 找到 activeElement

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

另一个焦点选项是 delegatesFocus: true 选项,它会展开 影子树中元素的焦点行为:

  • 如果您点击 shadow DOM 内的某个节点,但该节点不是可聚焦区域, 第一个可聚焦区域会变为聚焦状态。
  • 当 shadow DOM 内的节点获得焦点时,:focus 应用于以下范围内的主机: 对聚焦的元素进行补充

示例 - delegatesFocus: true 如何更改焦点行为

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

结果

delegatesFocus: 真实行为。

上方是聚焦 <x-focus>(用户点击、通过 Tab 键进入、 focus() 等)、"可点击的 Shadow DOM 文本"或内部的 <input> 已获得焦点(包括 autofocus)。

如果是设置 delegatesFocus: false,则将出现以下内容:

<ph type="x-smartling-placeholder">
</ph> delegatesFocus: false,且内部输入是聚焦的。 <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false 和内部 <input> 获得焦点。
<ph type="x-smartling-placeholder">
</ph> delegatesFocus:false 和 x-focus
    获得焦点(例如,它具有 tabindex=&#39;0&#39;)。 <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false<x-focus> 获得焦点(例如,它具有 tabindex="0")。
<ph type="x-smartling-placeholder">
</ph> delegatesFocus: false 和“Clickable Shadow DOM text”为
    (或点击元素 shadow DOM 中的其他空白区域)。 <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false 和“可点击的 Shadow DOM 文本”为 (或点击元素 shadow DOM 中的其他空白区域)。

提示和技巧

多年来,我学到了一些关于编写网络组件的知识。我 其中的一些提示对于编写组件和 如何调试 shadow DOM。

使用 CSS 组件

通常,网络组件的布局/样式/绘制相当独立。使用 :host 中的 CSS containment(用于性能) 胜出:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

重置可继承样式

可继承的样式(backgroundcolorfontline-height 等)继续 在 shadow DOM 中继承的方法。也就是说,它们突破了 shadow DOM 边界 默认值。如果您想从头开始,请使用 all: initial; 进行重置 当它们跨越阴影边界时,将可继承样式设置为其初始值。

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

查找网页使用的所有自定义元素

有时,查找页面中使用的自定义元素很有用。为此,您需要 需要以递归方式遍历页面上使用的所有元素的 shadow DOM。

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

通过 <template> 创建元素

我们不使用 .innerHTML 来填充影子根,而是使用声明式 <template>。模板是一种理想的占位符,用于声明 Web 组件。

有关示例,请参见 “自定义元素:构建可重复使用的网络组件”

历史记录和浏览器支持

如果您最近几年一直在关注网络组件, Chrome 35+/Opera 随附的是针对 一段时间。Blink 会继续同时支持两个版本 。v0 规范提供了创建影子根的不同方法 (element.createShadowRoot,而不是 v1 的 element.attachShadow)。调用 旧方法会继续创建具有 v0 语义的影子根,因此现有的 v0 它就不会被破坏

如果您对旧版 v0 规范感兴趣,请访问 html5rocks 文章: 123。 该帮助中心也提供了 shadow DOM v0 与 v1 之间的差异

浏览器支持

Chrome 53(状态)中提供了 Shadow DOM v1。 Opera 40、Safari 10 和 Firefox 63。边缘 已开始开发

如需检测 shadow DOM,请检查是否存在 attachShadow

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

聚酯纤维

在浏览器广泛支持之前, shadydomshadycss polyfill 为您提供 v1 功能。Shady DOM 可以模拟 Shadow DOM 和 shadycss polyfill 的 DOM 作用域 CSS 自定义属性和原生 API 提供的样式范围。

安装 polyfill:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

使用 polyfill:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

请参阅 https://github.com/webcomponents/shadycss#usage ,了解如何对样式进行填充/作用域设置。

总结

有史以来第一次,我们拥有了执行适当 CSS 作用域的 API 原语, DOM 作用域,并且具有真正的组合。与其他网络组件 API 结合使用 与自定义元素一样,shadow DOM 提供了一种真正封装 或者使用 <iframe> 等旧组件。

不要误会我的意思。Shadow DOM 无疑是一个复杂的巨兽!但它是个野兽 值得学习。请花些时间查看。了解并提出问题!

深入阅读

常见问题解答

我现在可以使用 Shadow DOM v1 吗?

使用 polyfill 时,答案是肯定的。请参阅浏览器支持

shadow DOM 提供哪些安全功能?

Shadow DOM 不是一项安全功能。它是一种用于限定 CSS 范围的轻量级工具 并在组件中隐藏 DOM 树。如果您想要一个真正的安全边界 使用 <iframe>

网络组件是否必须使用 shadow DOM?

不对!您无需创建使用 shadow DOM 的网络组件。不过, 编写使用 Shadow DOM 的自定义元素意味着您可以 充分利用 CSS 作用域、DOM 封装和组合等功能。

开放和闭合的影子根有何区别?

请参阅闭合的影子根