2020 年 12 月 6 日

Shadow DOM

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 子树

  1. 光树 - 一个普通的 DOM 子树,由 HTML 子元素组成。我们在前面章节中看到的所有子树都是“光”树。
  2. 影子树 - 一个隐藏的 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: …})创建一个影子树。

有两个限制

  1. 我们只能为每个元素创建一个影子根。
  2. 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 与主文档严格隔离

  1. Shadow DOM 元素对来自 light DOM 的 querySelector 不可見。特别是,Shadow DOM 元素可能具有与 light DOM 中的元素冲突的 id。它们必须在 shadow tree 内唯一。
  2. 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>
  1. 来自文档的样式不会影响 shadow tree。
  2. …但来自内部的样式有效。
  3. 要获取 shadow tree 中的元素,我们必须从 tree 内部进行查询。

参考资料

总结

Shadow DOM 是一种创建组件本地 DOM 的方法。

  1. shadowRoot = elem.attachShadow({mode: open|closed}) – 为 elem 创建 Shadow DOM。如果 mode="open",则它可以通过 elem.shadowRoot 属性访问。
  2. 我们可以使用 innerHTML 或其他 DOM 方法填充 shadowRoot

Shadow DOM 元素

  • 拥有自己的 id 空间,
  • 对来自主文档的 JavaScript 选择器(如 querySelector)不可见,
  • 仅使用来自 shadow tree 的样式,而不是来自主文档的样式。

如果存在,Shadow DOM 将由浏览器渲染,而不是所谓的“light DOM”(普通子元素)。在章节 Shadow DOM 插槽,组合 中,我们将看到如何组合它们。

教程地图

评论

在评论之前阅读...
  • 如果您有改进建议,请 提交 GitHub 问题 或拉取请求,而不是评论。
  • 如果您不理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请将它们包装在 <pre> 标签中,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)