Shadow DOM 基础知识

Dominic Cooney
Dominic Cooney

简介

Web 组件是一套尖端标准,具有以下特点:

  1. 使构建 widget 成为可能
  2. ...这些特征可以可靠地重复使用
  3. ...并且如果下一版本的组件 更改内部实现详情。

这是否意味着您必须决定何时使用 HTML/JavaScript,并且 何时使用 Web 组件?否!HTML 和 JavaScript 和互动的视觉元素微件是交互式可视化内容。它 您应该充分利用自己在 HTML 和 JavaScript 方面的技能, 开发 widget。Web 组件标准旨在帮助 你就是这么做的。

但有一个根本问题, HTML 和 JavaScript 很难使用: 封装在一起。这种封装的缺乏 意味着您的文档样式表可能会意外应用于 在 widget 内;您的 JavaScript 可能会意外修改某些部分, 在 widget 内;您的 ID 可能与微件内的 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 树没有封装,因此整个 DOM 结构的 名称标签会公开给文档如果网页上的其他元素 无意中使用相同的类名称进行样式设置或脚本, 会很糟糕。

我们可以避免麻烦。

第 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!”文本和 标记上。

作为组件的作者,您可以定义组合如何与 widget。<content>这个 在 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!“My name is”),但首先使用的是日语。 (在“CANNOT TRANSLATE”之前)。这种区分在语义上没有意义 从更新当前显示的名称的角度开始, 因此名称更新代码不必知道这些细节。

额外学分:高级预测

在上述示例中,<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 101

以上就是 Shadow DOM 的基础知识 - 您将通过 Shadow DOM 101 完成这一步!您可以 对 Shadow DOM 执行更多操作,例如,您可以在 一个影子主机,或者用于封装的嵌套阴影,或者架构师 使用模型驱动的视图 (MDV) 和 Shadow DOM。和 Web 组件不仅仅是 Shadow DOM。

我们将在后续博文中说明这一点。