Shadow DOM 基础知识

Dominic Cooney
Dominic Cooney

简介

Web 组件是一套先进标准,旨在:

  1. 让构建 widget 成为可能
  2. ...可以可靠地重复使用
  3. ...并且该组件不会在组件的下一个版本更改内部实现细节时中断页面。

这是否意味着您必须决定何时使用 HTML/JavaScript,以及何时使用 Web 组件?否!HTML 和 JavaScript 可以 生成交互式可视化内容widget 是交互式视觉内容。在开发 widget 时,充分利用您的 HTML 和 JavaScript 技能是明智之举。Web 组件标准旨在帮助您做到这一点。

但有一个根本问题,使得基于 HTML 和 JavaScript 构建的 widget 难以使用:widget 内的 DOM 树不会从页面的其余部分封装。这种没有封装意味着文档样式表可能会意外应用到 widget 内的部分;您的 JavaScript 可能会意外修改 widget 内的部分;您的 ID 可能会与 widget 内的 ID 重叠,等等。

Web 组件由三个部分组成:

  1. 模板
  2. 阴影 DOM
  3. 自定义元素

Shadow DOM 解决了 DOM 树封装问题。Web 组件的四个部分旨在协同工作,但您也可以挑选要使用的 Web 组件部分。本教程介绍了如何使用 Shadow DOM。

你好,影族世界

借助 Shadow DOM,元素可以获取一种新类型的节点。这种新型节点称为“影子根”。具有与其关联的影子根的元素称为影子主机。系统不会渲染影子主机的内容,而是渲染影子根的内容。

例如,如果您有如下标记:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

而不是

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

您的网页

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

不仅如此,如果页面上的 JavaScript 询问按钮的 textContent 是什么,那么便不会获得“こんちは、电影の世界!”;而是返回“Hello, world!”,因为影子根下的 DOM 子树已封装。

将内容与演示文稿分开

接下来,我们介绍如何使用 Shadow DOM 将内容与呈现分隔开来。假设我们有以下名称标签:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

这里是标记。这就是你今天要写的内容。因此它不使用 Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

由于 DOM 树缺少封装,因此 name 标记的整个结构都会向文档公开。如果网页上的其他元素无意中使用相同的类名称来设置样式或编写脚本,我们将感到很不舒服。

我们可以避免糟糕的时光。

第 1 步:隐藏演示文稿详细信息

在语义上,我们可能只关心:

  • 这是名称标签。
  • 名字是“Bob”。

首先,我们编写更接近于我们想要的真实语义的标记:

<div id="nameTag">Bob</div>

然后,我们将用于呈现的所有样式和 div 放入 <template> 元素中:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

此时,渲染的唯一对象是“Bob”。因为我们将展示性 DOM 元素移到了 <template> 元素内,所以它们不会被渲染,但可以通过 JavaScript 访问。我们现在执行该操作以填充影子根目录:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

现在,我们已经设置了影子根,名称标记将再次渲染。如果您右键点击名称标记并检查元素,您会看到它是甜美的语义标记:

<div id="nameTag">Bob</div>

这表明,通过使用 Shadow DOM,我们已在文档中隐藏了名称标签的呈现详情。演示详情封装在 Shadow DOM 中。

第 2 步:将内容与演示文稿分开

现在,我们的名称标记可以从页面中隐藏呈现详细信息,但实际上并不会将呈现与内容分离,因为虽然内容(“Bob”)位于网页中,但呈现的名称就是我们复制到影子根目录中的名称。如果要更改名称标签上的名称,需要在两个位置进行更改,这两个位置可能会不同步。

HTML 元素具有构成元素 - 例如,您可以将按钮放在表格中。在这里,我们需要的是组合:名称标签必须由红色背景、“Hi!”文本和名称标签上的内容组成。

作为组件的创作者,您可以使用名为 <content> 的新元素定义组合如何与 widget 协同工作。这会在 widget 呈现时创建一个插入点,并且该插入点会从阴影宿主中择优挑选内容并在该点呈现。

如果我们将 Shadow DOM 中的标记更改为这样:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

渲染名称标记时,影子主机的内容会投影到 <content> 元素显示的点。

现在,文档的结构更简单了,因为名称只在一个位置,即文档。如果您的页面需要更新用户的名称,只需编写以下代码:

document.querySelector('#nameTag').textContent = 'Shellie';

就是这么简单。浏览器会自动更新名称标记的呈现,因为我们使用 <content> 将名称标记的内容投影到位。

<div id="ex2b">

现在,我们已经实现了内容与呈现的分离。内容在文档中;演示在 Shadow DOM 中。 在需要渲染内容时,浏览器会自动使其保持同步。

第 3 步:利润

通过将内容与呈现内容分开,我们可以简化操控内容的代码。在名称标记示例中,该代码只需处理包含一个 <div>(而非多个)的简单结构。

现在,如果我们更改呈现方式,无需更改任何代码!

例如,假设我们要本地化名称标签。它仍然是名称标签,因此文档中的语义内容不会更改:

<div id="nameTag">Bob</div>

影子根设置代码保持不变。放入影子根的内容会发生变化:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

与如今的网络状况相比,这是一个很大的改进,因为名称更新代码可以依赖于简单且一致的组件结构。您的名称更新代码不需要知道用于渲染的结构。如果我们考虑渲染什么,这个名称在英语中会第二位显示(在“Hi!

额外学习内容:高级投影

在上面的示例中,<content> 元素从影子主机中择优挑选所有内容。您可以使用 select 属性来控制要预测的内容元素。您还可以使用多个内容元素。

例如,如果您有一个文档包含以下内容:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

以及一个使用 CSS 选择器选择特定内容的影子根:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

<div class="email"> 元素同时由 <content select="div"><content select=".email"> 元素匹配。小鲍的电子邮件地址出现了几次?用什么颜色显示?

答案是小鲍的电子邮件地址出现了一次,结果是黄色的。

原因在于,入侵 Shadow DOM 的人都知道,构建屏幕上实际渲染的内容树就像一个盛大的派对。内容元素是一个邀请内容,它可将文档中的内容传入后台 Shadow DOM 渲染方。这些邀请将按顺序发出,谁收到邀请取决于邀请对象(即 select 属性)。内容一旦被邀请,就会始终接受邀请(谁不愿意呢?!),并随即关闭。如果后续的邀请再次发送到该地址,那么家中无人,它也不会来到您的派对。

在上面的示例中,<div class="email"> 同时匹配 div 选择器和 .email 选择器,但由于带有 div 选择器的内容元素出现在文档中的前面,<div class="email"> 会去参加黄色派对,没有人可以来参加蓝色派对。(这也许就是为什么它如此蓝色,虽然苦难还喜欢陪伴,所以你永远不知道。)

如果某项内容被邀请到任何派对,则根本不会进行渲染。这就是第一个示例中“Hello, world”文本发生的情况。当您想要实现完全不同的渲染时,这非常有用:在文档中编写语义模型,这是网页中脚本可以访问的内容,但出于渲染目的而将其隐藏,并使用 JavaScript 将其连接到 Shadow DOM 中完全不同的渲染模型。

例如,HTML 有一个非常实用的日期选择器。输入 <input type="date">,即可获得整洁的弹出式日历。但是,如果您想让用户为他们的甜点岛假期选择日期范围(您知道...使用红藤制吊床),该怎么办?您可以按以下方式设置文档:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

但要创建 Shadow DOM 它使用表格来创建突出显示日期范围等的精美日历当用户点击日历中的日期时,该组件会更新 startDate 和 endDate 输入中的状态;当用户提交表单时,系统会提交这些输入元素中的值。

如果不呈现标签,为什么我在文档中添加了标签?这是因为,如果用户使用不支持 Shadow DOM 的浏览器查看表单,表单仍可使用,只是不够美观。用户会看到如下内容:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

你传递 Shadow DOM 基础知识

以上就是 Shadow DOM 的基础知识 - 您顺利通过了 Shadow DOM 101 入门!您可以使用 Shadow DOM 执行更多操作,例如可以在一个影子主机上使用多个阴影进行封装,或者使用模型驱动型视图 (MDV) 和 Shadow DOM 构建网页架构。网络组件不仅仅是 Shadow DOM。

我们将在后续博文中对此进行介绍。