专栏首页全栈修仙之路Shadow DOM v1 简介

Shadow DOM v1 简介

什么是 Shadow DOM

Shadow DOM 是 Web Components 定义的四大标准之一。Shadow DOM 解决了构建网络应用的脆弱性问题。脆弱性是由 HTML、CSS 和 JS 的全局性引起的。

Shadow DOM 为网络开发中的常见问题提供解决方案:

  • 隔离 DOM:组件的 DOM 是独立的(例如,document.querySelector() 不会返回组件 Shadow DOM 中的节点)。
  • 作用域 CSS:Shadow DOM 内部定义的 CSS 在其作用域内。样式规则不会泄漏,页面样式也不会渗入。
  • 组合:为组件设计一个声明性、基于标记的 API。
  • 简化 CSS: 作用域 DOM 意味着您可以使用简单的 CSS 选择器,更通用的 id/class 名称,而无需担心命名冲突。
  • 效率:将应用看成是多个 DOM 块,而不是一个大的(全局性)页面。

(图片来源 —— MDN Shadow DOM)

Shadow DOM vs DOM

HTML 因其易于使用的特点驱动着网络的发展。通过声明几个标记,即可在几秒内编写一个带有图文信息和结构的页面。 但是,HTML 自身的功能并不强大。 对于我们人类而言,理解基于文本语言很容易,但是机器需要更多帮助才能理解。 因此,文档对象模型(DOM) 应运而生。

Shadow DOM 与普通 DOM 相同,但有两点区别:

1) 创建/使用的方式;

2) 与页面其他部分有关的行为方式。

这里以 “创建/使用的方式” 为例:

创建 DOM

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello world!';
header.appendChild(h1);
document.body.appendChild(header);

创建 Shadow DOM

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

需要注意的是,并不是所有的元素都可以挂载 Shadow DOM,其主要原因是:

  • 浏览器已为该元素托管其自身的内部 shadow DOM(比如 textareainput)。
  • 让元素托管 shadow DOM 毫无意义 (比如 img)。

所以以下方法是行不通的:

document.createElement('input').attachShadow({mode: 'open'});

另外使用 Shadow DOM 时,有以下的注意事项:

  • Shadow DOM,一旦创建就无法删除,它只能用新的替换。
  • 要查看浏览器如何为 input 或 textarea 等元素实现 shadow DOM,对 Chrome 用户来说可以按照: DevTools > Settings > Preferences > Elements -> [x] Show user agent shadow DOM路径启用对应的选项。

Shadow DOM vs Light DOM

Light DOM

组件用户编写的标记,该 DOM 不在组件 shadow DOM 之内,它是元素实际的子项。

<button is="better-button">
  <img src="gear.svg" slot="icon">
  <span>Settings</span>
</button>

Shadow DOM

该 DOM 是由组件的作者编写。Shadow DOM 对于组件而言是本地的,它定义内部结构、作用域 CSS 并封装实现详情。它还可定义如何渲染由组件使用者编写的标记。

#shadow-root
  <style>...</style>
  <slot name="icon"></slot>
  <span id="wrapper">
    <slot>Button</slot>
  </span>

如何创建 Shadow DOM

<div class="dom"></div>

<script>
    let el = document.querySelector('.dom');
    el.attachShadow({ mode: 'open' });
    el.shadowRoot.innerHTML = 'Hi I am shadowed!';

    // 创建新的元素
    let hello = document.createElement('span');
    hello.textContent = 'Hi I am shadowed but wrapped in span';
    el.shadowRoot.appendChild(hello);
</script>

什么是 Shadow Root

ShadowRoot 是 Shadow DOM 的根,从技术上讲,它是一个非元素节点,是一种特殊的文档片段。你可以通过 ShadowRoot 对象上的 appendChildquerySelectorAll 等方法去操作整个 Shadow DOM 树。

对于一个普通的元素,比如 <div>,你可以通过调用该对象上的 attachShadow 方法来创建一个ShadowRoot,attachShadow 接受一个对象进行初始化,这个对象有一个 mode 属性,它有两个取值:'open''closed',这个属性是在创造 ShadowRoot 的时候需要初始化提供的,并在创建 ShadowRoot 之后成为一个只读属性。

那么 mode: 'open'mode: 'closed' 有什么区别呢?

let $element = document.createElement("div");
$element.attachShadow({ mode: "open" });
$element.shadowRoot

在调用 attachShadow 创建 ShadowRoot 之后,attachShdow 方法会返回 ShadowRoot 对象实例,你可以通过这个返回的对象去构造整个 Shadow DOM。

当 mode 为 'open' 时,在用于创建 ShadowRoot 的外部普通节点(比如 <div>)上,会有一个 shadowRoot 属性,这个属性也就是创造出来的那个 ShadowRoot,也就是说,你可以通过这个属性获取 ShadowRoot,进而对它进行操作。

而当 mode 为 'closed' 时,你将不能再得到这个属性,这个属性会被设置为 null,比如:

let $element = document.createElement("div");
$element.attachShadow({ mode: "closed" });
$element.shadowRoot // null

一般情况下总是使用 open 模式调用 attachShadow 方法,这样的话可以让组件作者和用户都可以根据需要进行相关操作。

如何设定样式

Shadow DOM 最有用的功能是作用域 CSS:

  • 外部页面中的 CSS 选择器不应用于组件内部。
  • 内部定义的样式也不会渗出,它们的作用域仅限于宿主元素。

设置宿主元素样式

<style>
:host {
  display: block; 
  contain: content; 
}
</style>

使用 :host 的一个问题是,父页面中的规则较之在元素中定义的 :host 规则具有更高的特异性。 也就是说,外部样式优先。这可让用户从外部替换你已定义的样式。 此外,:host 仅在影子根范围内起作用,因此无法在 shadow DOM 之外使用。

除了 :host 之外,还支持 :host(<selector>) 的函数形式,它可让你基于宿主将对用户互动或状态的反应行为进行封装,或对内部节点进行样式设定。

<style>
:host {
  opacity: 0.4;
  will-change: opacity;
  transition: opacity 300ms ease-in-out;
}
:host(:hover) {
  opacity: 1;
}
:host([disabled]) {
  background: grey;
  pointer-events: none;
  opacity: 0.4;
}
:host(.blue) {
  color: blue; 
}
:host(.pink) > #tabs {
  color: pink;
</style>

基于情景设定样式

如果 :host-context(<selector>) 或其任意父级与 <selector> 匹配,它将与组件匹配。 一个常见用途是根据组件的环境进行主题化。 例如,很多人都通过将 class 应用到 <html><body> 进行主题化:

<body class="darktheme">
  <fancy-tabs>
    ...
  </fancy-tabs>
</body>

如果 :host-context(.darktheme).darktheme 的子级,它将对 <fancy-tabs> 进行样式化:

:host-context(.darktheme) {
  color: white;
  background: black;
}

为分布式节点设定样式

比如说我们已创建了一个 name badge 组件:

<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;
}
    
/* 以下方式不生效,因为::slotted()只支持顶层元素    
::slotted(.company),
::slotted(.title .company) {
  text-transform: uppercase;
}
*/
</style>
<slot></slot>

从外部为组件设定样式

有几种方法可从外部为组件设定样式:最简单的方法是使用标签名称作为选择器:

fancy-tabs {
  width: 500px;
  color: red;
}

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

浏览器支持

(图片来源 —— https://caniuse.com/#feat=shadowdomv1)

由上图可知 Chrome 53、Opera 40 和 Safari 10 以上的版本是支持 shadow DOM v1 标准。 Edge 也在考虑中,并具有较高的优先级

你可以通过以下方法来检测当前浏览器是否支持 shadow DOM v1 标准:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

此外对于还不支持 shadow DOM v1 标准的浏览器来说,你也可以引入 shadydomshadycss polyfill 来模拟 v1 的标准。Shady DOM 可以模拟 Shadow DOM 的 DOM 作用域,而 shadycss polyfill 则可以模拟原生 API 提供的 CSS 自定义属性和样式作用域。具体的使用方式,感兴趣的同学,请参考相应的开发文档,这里不再进一步说明。

参考资源

  • 深度介绍:你听说过原生 HTML 组件吗?
  • Shadow DOM v1:独立的网络组件

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 你不知道的 MutationObserver

    在某些场景下,我们希望能监视 DOM 树的变动,然后做一些相关的操作。比如监听元素被插入 DOM 或从 DOM 树中移除,然后添加相应的动画效果。或者在富文本编...

    阿宝哥
  • TS 的构造签名和构造函数类型是啥?傻傻分不清楚

    以上接口中的 new (x: number, y: number) 我们称之为构造签名,其语法如下:

    阿宝哥
  • 理解 TypeScript 类型收窄

    TypeScript 类型收窄就是从宽类型转换成窄类型的过程。类型收窄常用于处理联合类型变量的场景,一个常见的例子是非空检查:

    阿宝哥
  • 增量 DOM 与虚拟 DOM 的对比使用

    如果你熟悉 React,你大概听说过虚拟 DOM 的概念。React 受欢迎的主要原因之一就是通过虚拟 DOM 提高用户界面性能。

    桃翁
  • React虚拟DOM的理解

    Virtual DOM是一棵以JavaScript对象作为基础的树,每一个节点可以将其称为VNode,用对象属性来描述节点,实际上它是一层对真实DOM的抽象,最...

    WindrunnerMax
  • 从 React 历史的长河里聊虚拟DOM及其价值

    本质上是 JavaScript 对象,这个对象就是更加轻量级的对 DOM 的描述。

    前端劝退师
  • Shadow DOM 初探

    Shadow DOM用官方的翻译来说就是影子 DOM,他能够为Web组件中的 DOM和 CSS提供了封装,实际上是在浏览器渲染文档的时候会给指定的DOM结构插入...

    IMWeb前端团队
  • 现代前端技术解析:现代前端交互框架

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

    奋飛
  • Shadow DOM简介

    一个网页会形成一颗DOM树,树上有很多结点,但过多的结点使结构复杂起来,Shadow DOM允许我们用封装的方式简化这种结构。Shadow DOM是网页DOM树...

    gojam
  • Virtual DOM

    DOM操作太消耗浏览器计算资源,diff算法才有其存在的意义。 DOM操作为什么慢? 1.直观感受:在浏览器控制台,用for循环遍历div属性,会看到有很多...

    城市中的游牧民族

扫码关注云+社区

领取腾讯云代金券