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 上的完整源代码。
什么是 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'}
:
人为的安全感。没有什么能够阻止攻击者 盗用
Element.prototype.attachShadow
。闭合模式会阻止自定义元素代码访问其自己的 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'); } ... });
闭合模式会降低组件对最终用户的灵活性。您 但有时您可能会忘记添加 功能。配置选项。用户想要的用例。常见 例如忘记为内部节点添加足够的样式钩子。 在闭合模式下,用户无法覆盖默认设置并进行调整 样式。能够访问组件的内部内容非常有帮助。 最终,用户会复刻您的组件、寻找其他组件或创建自己的组件 自己就会成为客户的 :(
在 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] |
<my-component></my-component> | slot.assignedNodes(); |
[] |
<my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
元素分配到哪个位置?
也可以回答反向问题。element.assignedSlot
会告知
该元素会分配到哪个组件槽位。
Shadow DOM 事件模型
当事件从 shadow DOM 中触发时,其目标会进行调整,以维持 封装。也就是说,系统会重新定位事件 好像它们来自组件,而不是内部元素 shadow DOM。有些事件甚至不会从 shadow DOM 中传播出去。
确实会跨越影子边界的事件包括:
- 焦点事件:
blur
、focus
、focusin
、focusout
- 鼠标事件:
click
、dblclick
、mousedown
、mouseenter
、mousemove
等 - 滚轮事件:
wheel
- 输入事件:
beforeinput
、input
- 键盘事件:
keydown
、keyup
- 组合事件:
compositionstart
、compositionupdate
、compositionend
- DragEvent:
dragstart
、drag
、dragend
、drop
等
提示
如果影子树处于打开状态,调用 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: 真实行为。](https://web.developers.google.cn/static/articles/shadowdom-v1/image/delegatesfocus-true-beha-2829e235044ab.png?authuser=0000&hl=fi)
上方是聚焦 <x-focus>
(用户点击、通过 Tab 键进入、
focus()
等)、"可点击的 Shadow DOM 文本"或内部的
<input>
已获得焦点(包括 autofocus
)。
如果是设置 delegatesFocus: false
,则将出现以下内容:
![delegatesFocus: false,且内部输入是聚焦的。](https://web.developers.google.cn/static/articles/shadowdom-v1/image/delegatesfocus-false-th-a0624bcb139e9.png?authuser=0000&hl=fi)
delegatesFocus: false
和内部 <input>
获得焦点。
![delegatesFocus:false 和 x-focus
获得焦点(例如,它具有 tabindex='0')。](https://web.developers.google.cn/static/articles/shadowdom-v1/image/delegatesfocus-false-x-ad7729291b314.png?authuser=0000&hl=fi)
delegatesFocus: false
和<x-focus>
获得焦点(例如,它具有 tabindex="0"
)。
![delegatesFocus: false 和“Clickable Shadow DOM text”为
(或点击元素 shadow DOM 中的其他空白区域)。](https://web.developers.google.cn/static/articles/shadowdom-v1/image/delegatesfocus-false-c-29c711da9b14f.png?authuser=0000&hl=fi)
delegatesFocus: false
和“可点击的 Shadow DOM 文本”为
(或点击元素 shadow DOM 中的其他空白区域)。
提示和技巧
多年来,我学到了一些关于编写网络组件的知识。我 其中的一些提示对于编写组件和 如何调试 shadow DOM。
使用 CSS 组件
通常,网络组件的布局/样式/绘制相当独立。使用
:host
中的 CSS containment(用于性能)
胜出:
<style>
:host {
display: block;
contain: content; /* Boom. CSS containment FTW. */
}
</style>
重置可继承样式
可继承的样式(background
、color
、font
、line-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 文章: 1、 2、 3。 该帮助中心也提供了 shadow DOM v0 与 v1 之间的差异。
浏览器支持
Chrome 53(状态)中提供了 Shadow DOM v1。 Opera 40、Safari 10 和 Firefox 63。边缘 已开始开发。
如需检测 shadow DOM,请检查是否存在 attachShadow
:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
聚酯纤维
在浏览器广泛支持之前, shadydom 和 shadycss 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 与 v0 之间的区别
- “基于槽位的 Shadow DOM API 简介” 。
- Web 组件和模块化 CSS 的未来 作者:Philip Walton
- “自定义元素:构建可重复使用的网络组件” 来自 Google WebFundamentals 的视频
- Shadow DOM v1 规范
- 自定义元素 v1 规范
常见问题解答
我现在可以使用 Shadow DOM v1 吗?
使用 polyfill 时,答案是肯定的。请参阅浏览器支持。
shadow DOM 提供哪些安全功能?
Shadow DOM 不是一项安全功能。它是一种用于限定 CSS 范围的轻量级工具
并在组件中隐藏 DOM 树。如果您想要一个真正的安全边界
使用 <iframe>
。
网络组件是否必须使用 shadow DOM?
不对!您无需创建使用 shadow DOM 的网络组件。不过, 编写使用 Shadow DOM 的自定义元素意味着您可以 充分利用 CSS 作用域、DOM 封装和组合等功能。
开放和闭合的影子根有何区别?
请参阅闭合的影子根。