上文中我们讲解了 DOM 中最重要的节点类型之一的 Document 节点类型,本文我们继续深入,谈谈另一个重要的节点类型 Element 。 1、概况 Element 类型用于表现 HTML 或 XML…
深入理解Shadow DOM, 什么是 shadow DOM, shadow DOM入门, 影子 DOM(Shadow DOM)
shadow DOM不是超级英雄电影中的恶棍,也不是DOM的黑暗面。 shadow DOM只是一种解决文档对象模型(或简称DOM)中缺少的树封装方法。
网页通常使用来自外部源的数据和小部件,如果它们没有封装,那么样式可能会影响HTML中不必要的部分,迫使开发人员使用特定的选择器和!important
规则来避免样式冲突。
尽管如此,在编写大型程序时,这些努力似乎并不是那么有效,并且大量的时间被浪费在防止CSS和JavaScript的冲突上。 Shadow DOM API旨在通过提供封装DOM树的机制来解决这些问题。
Shadow DOM是用于创建Web组件的主要技术之一,另外两个是自定义元素和HTML模板。 Web 组件的规范最初是由Google提出的,用于简化Web小部件的开发。
虽然这三种技术旨在协同工作,不过你可以自由地分别使用每种技术。本教程的范围仅限于shadow DOM。
什么是DOM?
在深入研究如何创建shadow DOM之前,了解DOM是什么非常重要。 W3C文档对象模型(DOM)提供了一个平台和语言无关的应用程序编程接口(API),用于表示和操作存储在HTML和XML文档中的信息。
通过使用DOM,程序员可以访问、添加、删除或更改元素和内容。 DOM将网页视为树结构,每个分支以节点结束,每个节点包含一个对象,可以使用JavaScript等脚本语言对其进行修改。
考虑以下HTML文档:
<html> <head> <title>Sample document</title> </head> <body> <h1>Heading</h1> <a href="https://example.com">Link</a> </body> </html>

此图中所有的框都是节点。
用于描述DOM部分的术语类似于现实世界中的家谱树:
- 给定节点上一级节点是该节点的父节点
- 给定节点下一级节点是该节点的子节点
- 具有相同父级的节点是兄弟节点
- 给定节点上方的所有节点(包括父节点和祖父节点)都称为该节点的祖先
- 最后,给定节点下所有的节点都被称为该节点的后代
节点的类型取决于它所代表的HTML元素的类型。 HTML标记被称为元素节点。嵌套标签形成一个元素树。元素中的文本称为文本节点。文本节点可能没有子节点,你可以把它想象成是一棵树的叶子。
为了访问树,DOM提供了一组方法,程序员可以用这些方法修改文档的内容和结构。例如当你写下document.createElement('p');
时,就在使用DOM提供的方法。没有DOM,JavaScript就无法理解HTML和XML文档的结构。
下面的JavaScript代码显示了如何使用DOM方法创建两个HTML元素,将一个嵌套在另一个内部并设置文本内容,最后把它们附加到文档正文:
const section = document.createElement('section'); const p = document.createElement('p'); p.textContent = 'Hello!'; section.appendChild(p); document.body.appendChild(section);
这是运行这段JavaScript代码后生成的DOM结构:
<body> <section> <p>Hello!</p> </section> </body>
什么是 shadow DOM?
封装是面向对象编程的基本特性,它使程序员能够限制对某些对象组件的未授权访问。
在此定义下,对象以公共访问方法的形式提供接口作为与其数据交互的方式。这样对象的内部表示不能直接被对象的外部访问。
Shadow DOM将此概念引入HTML。它允许你将隐藏的,分离的DOM链接到元素,这意味着你可以使用HTML和CSS的本地范围。现在可以用更通用的CSS选择器而不必担心命名冲突,并且样式不再泄漏或被应用于不恰当的元素。
实际上,Shadow DOM API正是库和小部件开发人员将HTML结构、样式和行为与代码的其他部分分开所需的东西。
Shadow root 是 shadow 树中最顶层的节点,是在创建 shadow DOM 时被附加到常规DOM节点的内容。具有与之关联的shadow root的节点称为shadow host。
你可以像使用普通DOM一样将元素附加到shadow root。链接到shadow root的节点形成 shadow 树。通过图表应该能够表达的更清楚:

术语light DOM通常用于区分正常DOM和shadow DOM。shadow DOM和light DOM被并称为逻辑DOM。light DOM与shadow DOM分离的点被称为阴影边界。 DOM查询和CSS规则不能到达阴影边界的另一侧,从而创建封装。
Shadow DOM 为封装而生。它可以让一个组件拥有自己的「影子」DOM 树,这个 DOM 树不能在主文档中被任意访问,可能拥有局部样式规则,还有其他特性。
内建 shadow DOM
你是否曾经思考过复杂的浏览器控件是如何被创建和添加样式的?
比如 <input type="range">
:
浏览器在内部使用 DOM/CSS 来绘制它们。这个 DOM 结构一般来说对我们是隐藏的,但我们可以在开发者工具里面看见它。比如,在 Chrome 里,我们需要打开「Show user agent shadow DOM」选项。
然后 <input type="range">
看起来会像这样:
你在 #shadow-root
下看到的就是被称为「shadow DOM」的东西。
我们不能使用一般的 JavaScript 调用或者选择器来获取内建 shadow DOM 元素。它们不是常规的子元素,而是一个强大的封装手段。
在上面的例子中,我们可以看到一个有用的属性 pseudo
。这是一个因为历史原因而存在的属性,并不在标准中。我们可以使用它来给子元素加上 CSS 样式,像这样:
概况
本文章假设你对 DOM(文档对象模型)有一定的了解,它是由不同的元素节点、文本节点连接而成的一个树状结构,应用于标记文档中(例如 Web 文档中常见的 HTML 文档)。请看如下示例,一段 HTML 代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Simple DOM example</title> </head> <body> <section> <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth."> <p>Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a></p> </section> </body> </html>
这个片段会生成如下的 DOM 结构:

Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。

这里,有一些 Shadow DOM 特有的术语需要我们了解:
- Shadow host:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。
- Shadow tree:Shadow DOM内部的DOM树。
- Shadow boundary:Shadow DOM结束的地方,也是常规 DOM开始的地方。
- Shadow root: Shadow tree的根节点。
你可以使用同样的方式来操作 Shadow DOM,就和操作常规 DOM 一样——例如添加子节点、设置属性,以及为节点添加自己的样式(例如通过 element.style
属性),或者为整个 Shadow DOM 添加样式(例如在 <style>
元素内添加样式)。不同的是,Shadow DOM 内部的元素始终不会影响到它外部的元素(除了 :focus-within
),这为封装提供了便利。
注意,不管从哪个方面来看,Shadow DOM 都不是一个新事物——在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的 <video>
元素为例。你所能看到的只是一个 <video>
标签,实际上,在它的 Shadow DOM 中,包含来一系列的按钮和其他控制器。Shadow DOM 标准允许你为你自己的元素(custom element)维护一组 Shadow DOM。
基本用法
可以使用 Element.attachShadow()
方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode
属性,值可以是 open
或者 closed
:
let shadow = elementRef.attachShadow({mode: 'open'}); let shadow = elementRef.attachShadow({mode: 'closed'});
open
表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用 Element.shadowRoot
属性:
let myShadowDom = myCustomElem.shadowRoot;
如果你将一个 Shadow root 附加到一个 Custom element 上,并且将 mode
设置为 closed
,那么就不可以从外部获取 Shadow DOM 了——myCustomElem.shadowRoot
将会返回 null
。浏览器中的某些内置元素就是如此,例如<video>
,包含了不可访问的 Shadow DOM。
备注: 正如这篇文章所阐述的,处理 closed Shadow DOM 实际上很简单,往往也很值得这么做。
如果你想将一个 Shadow DOM 附加到 custom element 上,可以在 custom element 的构造函数中添加如下实现(目前,这是 shadow DOM 最实用的用法):
let shadow = this.attachShadow({mode: 'open'});
将 Shadow DOM 附加到一个元素之后,就可以使用 DOM APIs对它进行操作,就和处理常规 DOM 一样。
var para = document.createElement('p'); shadow.appendChild(para); etc.
编写简单示例
现在,让我们着手实现一个示例——<popup-info-box>
(也可以查看在线示例),来说明 Shadow DOM 在 custom element 中的实际运用。它包含一个图片 icon 和一段文字,并将 icon 嵌入页面之中。每当 icon 获取到焦点时,文字会在一个弹框中显示,以提供更加详细的信息。首先,在 JavaScript 文件中,我们需要定义一个叫做 PopUpInfo
的类,它继承自 HTMLElement
:
class PopUpInfo extends HTMLElement { constructor() { // 必须首先调用 super方法 super(); // 元素的具体功能写在下面 ... } }
在上面的类中,我们将会在它的构造函数中定义元素所有的功能。当类实例化后,所有的实例元素都会有相同功能。
创建 shadow root
在构造函数中,我们首先将 Shadow root 附加到 custom element 上:
// 创建 shadow root var shadow = this.attachShadow({mode: 'open'});
创建 shadow DOM 结构
接下来,我们会使用相关 DOM 操作来创建元素的 Shadow DOM 结构:
// 创建 span var wrapper = document.createElement('span'); wrapper.setAttribute('class','wrapper'); var icon = document.createElement('span'); icon.setAttribute('class','icon'); icon.setAttribute('tabindex', 0); var info = document.createElement('span'); info.setAttribute('class','info'); // 获取属性的内容并将内容添加到 info 元素内 var text = this.getAttribute('text'); info.textContent = text; // 插入 icon var imgUrl; if(this.hasAttribute('img')) { imgUrl = this.getAttribute('img'); } else { imgUrl = 'img/default.png'; } var img = document.createElement('img'); img.src = imgUrl; icon.appendChild(img);
为 shadow DOM 添加样式
之后,我们将要创建 <style>
元素,并加入一些 CSS 样式:
// 为 shadow DOM 添加一些 CSS 样式 var style = document.createElement('style'); style.textContent = ` .wrapper { position: relative; } .info { font-size: 0.8rem; width: 200px; display: inline-block; border: 1px solid black; padding: 10px; background: white; border-radius: 10px; opacity: 0; transition: 0.6s all; position: absolute; bottom: 20px; left: 10px; z-index: 3; } img { width: 1.2rem; } .icon:hover + .info, .icon:focus + .info { opacity: 1; }`;
将 Shadow DOM 添加到 Shadow root 上
最后,将所有创建的元素添加到 Shadow root 上:
// 将所创建的元素添加到 Shadow DOM 上 shadow.appendChild(style); shadow.appendChild(wrapper); wrapper.appendChild(icon); wrapper.appendChild(info);
使用我们的 custom element
完成类的定义之后,使用元素也是一样简单,只需将 custom element 放在页面上,正如 Using custom elements 中讲解的那样:
// 定义新的元素 customElements.define('popup-info', PopUpInfo);
<popup-info img="img/alt.png" text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the back of your card.">
使用外部引入的样式
在上面的示例中,我们使用行内<style>
元素为Shadow DOM添加样式,但是完全可以通过<link>
标签引用外部样式表来替代行内样式。
例如,我们可以看下 popup-info-box-external-stylesheet 例子 (查看源代码 source code):
// 将外部引用的样式添加到 Shadow DOM 上 const linkElem = document.createElement('link'); linkElem.setAttribute('rel', 'stylesheet'); linkElem.setAttribute('href', 'style.css'); // 将所创建的元素添加到 Shadow DOM 上 shadow.appendChild(linkElem);
请注意, 因为<link>
元素不会打断 shadow root 的绘制, 因此在加载样式表时可能会出现未添加样式内容(FOUC),导致闪烁。
许多现代浏览器都对从公共节点克隆的或具有相同文本的<style>
标签实现了优化,以允许它们共享单个支持样式表,通过这种优化,外部和内部样式的性能表现比较接近。
参见
本文:深入理解Shadow DOM, 什么是 shadow DOM, shadow DOM入门, 影子 DOM(Shadow DOM)