Shadow DOM 201

CSS 和样式

本文将详细讨论您可以使用 Shadow DOM 实现的更多精彩功能。 本文档以 Shadow DOM 101 中讨论的概念为基础。如果您想了解相关简介,请参阅该文章。

简介

毫无疑问,无样式的标记毫无吸引力。幸运的是,Web 组件背后的聪明人预见到了这一点,没有让我们陷入困境。CSS 作用域模块定义了许多用于为 shadow tree 中的内容设置样式的选项。

样式封装

Shadow DOM 的核心功能之一是 shadow 边界。它有很多不错的属性,但其中一个最棒的属性是它免费提供样式封装。换句话说:

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

关于此演示,有两点值得注意:

  • 此页面上还有其他 h3,但只有 ShadowRoot 中的 h3 与 h3 选择器匹配,因此只有该 h3 会被设置为红色。同样,默认情况下,样式是限定范围的。
  • 此页面上定义的其他针对 h3 的样式规则不会影响我的内容。 这是因为选择器不会跨越影子边界。

这个案例告诉我们,我们有来自外部世界的样式封装。感谢 Shadow DOM!

设置宿主元素的样式

借助 :host,您可以选择托管 shadow tree 的元素并为其设置样式:

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

需要注意的是,父页面中的规则比元素中定义的 :host 规则具有更高的特异性,但比特异性低于宿主元素上定义的 style 属性。这样,用户就可以从外部替换您的样式。 :host 也只能在 ShadowRoot 的上下文中使用,因此您无法在 Shadow DOM 之外使用它。

:host(<selector>) 的函数形式允许您定位与 <selector> 匹配的宿主元素。

示例 - 仅当元素本身具有类 .different(例如 <x-foo class="different"></x-foo>)时才匹配:

:host(.different) {
    ...
}

根据用户状态采取相应操作

:host 的一个常见用例是,您在创建自定义元素时,希望对不同的用户状态(:hover、:focus、:active 等)做出响应。

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

为元素设置主题

如果宿主元素或其任何祖先元素与 <selector> 匹配,则 :host-context(<selector>) 伪类与宿主元素匹配。

:host-context() 的一个常见用途是根据元素周围的环境为其设置主题。例如,许多人通过将类应用于 <html> 或 <body> 来设置主题:

<body class="different">
  <x-foo></x-foo>
</body>

当 <x-foo> 是具有类 .different 的元素的后代时,您可以使用 :host-context(.different) 为其设置样式:

:host-context(.different) {
  color: red;
}

这样,您就可以在元素的 Shadow DOM 中封装样式规则,从而根据其上下文对其进行独特样式设置。

支持在单个 shadow root 中使用多种宿主类型

:host 的另一个用途是,如果您要创建主题库,并希望支持在同一 Shadow DOM 中设置多种类型宿主元素的样式。

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

从外部设置 Shadow DOM 内部元素的样式

::shadow 伪元素和 /deep/ 组合器就像拥有 CSS 权威的 Vorpal 剑。 它们允许穿透 Shadow DOM 的边界,为 shadow tree 中的元素设置样式。

::shadow 伪元素

如果元素至少有一个 shadow tree,则 ::shadow 伪元素会与 shadow root 本身匹配。 它允许您编写选择器,以便为元素 Shadow DOM 内部的节点设置样式。

例如,如果某个元素托管了 shadow root,您可以编写 #host::shadow span {} 来为其 shadow tree 中的所有 span 设置样式。

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

示例(自定义元素)- <x-tabs> 的 Shadow DOM 中有 <x-panel> 个子元素。每个面板都托管自己的影子树,其中包含 h2 标题。如需在主页面中设置这些标题的样式,可以编写以下代码:

x-tabs::shadow x-panel::shadow h2 {
    ...
}

/deep/ 组合器

/deep/ 组合器与 ::shadow 类似,但功能更强大。它会完全忽略所有影子边界,并穿过任意数量的影子树。简而言之,/deep/ 允许您深入了解元素的内部结构并定位任何节点。

/deep/ 组合器在自定义元素领域特别有用,因为在自定义元素领域,通常会有多个级别的 Shadow DOM。主要示例包括嵌套一堆自定义元素(每个元素都托管自己的 shadow 树)或使用 <shadow> 创建继承自另一个元素的元素。

示例(自定义元素)- 选择树中任意位置作为 <x-tabs> 后代的所有 <x-panel> 元素:

x-tabs /deep/ x-panel {
    ...
}

示例 - 为 shadow tree 中任意位置具有 .library-theme 类的所有元素设置样式:

body /deep/ .library-theme {
    ...
}

使用 querySelector()

正如 .shadowRoot 会打开 shadow tree 以进行 DOM 遍历一样,组合器也会打开 shadow tree 以进行选择器遍历。您可以编写一条语句,而不是编写嵌套的疯狂链:

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

设置原生元素样式

原生 HTML 控件很难设置样式。许多人干脆放弃,选择自行开发。不过,借助 ::shadow 和 /deep/,Web 平台中任何使用 Shadow DOM 的元素都可以设置样式。<input> 类型和 <video> 就是很好的例子:

video /deep/ input[type="range"] {
  background: hotpink;
}

创建样式钩子

定制化是好事。在某些情况下,您可能希望在 Shadow 的样式防护罩中开孔,并创建钩子供其他人设置样式。

使用 ::shadow 和 /deep/

/deep/背后有强大的力量。它让组件作者能够将单个元素指定为可设置样式,或将一系列元素指定为可设置主题。

示例 - 为具有 .library-theme 类的所有元素设置样式,忽略所有 shadow tree:

body /deep/ .library-theme {
    ...
}

使用自定义伪元素

WebKit 和 Firefox 都定义了伪元素,用于设置原生浏览器元素内部部分的样式。input[type=range]就是一个很好的例子。您可以通过定位 ::-webkit-slider-thumb 来设置滑块滑块 <span style="color:blue">blue</span> 的样式:

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

与浏览器为某些内部元素提供样式钩子的方式类似,Shadow DOM 内容的作者可以将某些元素指定为可由外部人员设置样式。这是通过自定义伪元素完成的。

您可以使用 pseudo 属性将某个元素指定为自定义伪元素。 其值或名称需要带有“x-”前缀。这样做会与影子树中的该元素建立关联,并为外部人员提供一条跨越影子边界的指定通道。

以下示例展示了如何创建自定义滑块微件,并允许用户将其滑块滑块样式设置为蓝色:

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

使用 CSS 变量

创建主题钩子的有效方法是通过 CSS 变量。本质上,就是为其他用户创建“样式占位符”以供其填充。

假设自定义元素作者在其 Shadow DOM 中标记出变量占位符。一个用于设置内部按钮的字体样式,另一个用于设置其颜色:

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

然后,元素的嵌入器会根据自己的喜好定义这些值。也许是为了与他们自己网页上超酷的 Comic Sans 主题相匹配:

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

由于 CSS 变量的继承方式,一切都很顺利,而且效果很好!整个过程如下图所示:

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

重置样式

字体、颜色和行高等可继承样式会继续影响 Shadow DOM 中的元素。不过,为了实现最大程度的灵活性,Shadow DOM 为我们提供了 resetStyleInheritance 属性,用于控制在 shadow 边界发生的情况。您可以将其视为在创建新组件时重新开始的一种方式。

resetStyleInheritance

  • false - 默认。可继承的 CSS 属性会继续继承。
  • true - 在边界处将可继承属性重置为 initial。

以下演示展示了更改 resetStyleInheritance 对影子树的影响:

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
DevTools 继承的属性

理解 .resetStyleInheritance 稍微有些棘手,主要是因为它只会影响可继承的 CSS 属性。它表示:当您在页面和 ShadowRoot 之间的边界处寻找要继承的属性时,不要从宿主继承值,而是使用 initial 值(根据 CSS 规范)。

如果您不确定哪些属性会在 CSS 中继承,请查看这份实用列表,或在“元素”面板中勾选“显示继承的属性”复选框。

设置分布式节点的样式

分布式节点是在插入点(<content> 元素)处呈现的元素。借助 <content> 元素,您可以从 Light DOM 中选择节点,并将其呈现在 Shadow DOM 中的预定义位置。它们在逻辑上不在 Shadow DOM 中,仍然是宿主元素的子元素。插入点只是一个渲染机制。

分布式节点会保留主文档中的样式。也就是说,主页中的样式规则会继续应用于这些元素,即使它们在插入点呈现也是如此。 同样,分布式节点在逻辑上仍位于 Light DOM 中,不会移动。它们只是在其他地方呈现。不过,当节点分布到 Shadow DOM 中时,它们可以采用在 shadow 树中定义的其他样式。

::content 伪元素

分布式节点是宿主元素的子元素,那么我们如何从 Shadow DOM 内部定位它们?答案是 CSS ::content 伪元素。 这是一种定位通过插入点的 Light DOM 节点的方法。例如:

::content > h3 会为通过插入点的任何 h3 标记设置样式。

我们来看一个示例:

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

重置插入点处的样式

创建 ShadowRoot 时,您可以选择重置继承的样式。<content> 和 <shadow> 插入点也具有此选项。使用这些元素时,请在 JS 中设置 .resetStyleInheritance,或在元素本身上使用布尔值 reset-style-inheritance 属性。

  • 对于 ShadowRoot 或 <shadow> 插入点:reset-style-inheritance 表示可继承的 CSS 属性在到达影子内容之前,会在宿主处设置为 initial。此位置称为上边界。

  • 对于 <content> 插入点:reset-style-inheritance 表示在宿主的子级分布在插入点之前,可继承的 CSS 属性设置为 initial。此位置称为下边界。

总结

作为自定义元素的作者,我们有许多选项可用于控制内容的外观和风格。Shadow DOM 为这个全新的世界奠定了基础。

Shadow DOM 为我们提供了作用域样式封装,以及一种让我们选择尽可能多(或尽可能少)地让外部世界进入的方法。通过定义自定义伪元素或添加 CSS 变量占位符,作者可以为第三方提供便捷的样式钩子,以便进一步自定义其内容。总而言之,Web 作者可以完全控制其内容的呈现方式。