给 Shadow DOM 添加样式

153 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情 shadow DOM 可以包含 <style> 和 <link rel="stylesheet" href="…"> 标签。在后一种情况下,样式表是 HTTP 缓存的,因此不会为使用同一模板的多个组件重新下载样式表。

一般来说,局部样式只在 shadow 树内起作用,文档样式在 shadow 树外起作用。但也有少数例外。

:host

:host 选择器允许选择 shadow 宿主(包含 shadow 树的元素)。

例如,我们正在创建 <custom-dialog> 元素,并且想使它居中。为此,我们需要对 <custom-dialog> 元素本身设置样式。

这正是 :host 所能做的:

<template id="tmpl">
  <style>
    /* 这些样式将从内部应用到 custom-dialog 元素上 */
    :host {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>
</template>

<script>
customElements.define('custom-dialog', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
  }
});
</script>

<custom-dialog>
  Hello!
</custom-dialog>

级联

shadow 宿主( <custom-dialog> 本身)驻留在 light DOM 中,因此它受到文档 CSS 规则的影响。

如果在局部的 :host 和文档中都给一个属性设置样式,那么文档样式优先。

例如,如果在文档中我们有如下样式:

<style>
custom-dialog {
  padding: 0;
}
</style>

……那么 <custom-dialog> 将没有 padding。

这是非常有利的,因为我们可以在其 :host 规则中设置 “默认” 组件样式,然后在文档中轻松地覆盖它们。

唯一的例外是当局部属性被标记 !important 时,对于这样的属性,局部样式优先。

:host(selector)

与 :host 相同,但仅在 shadow 宿主与 selector 匹配时才应用样式。

例如,我们希望仅当 <custom-dialog> 具有 centered 属性时才将其居中:

<template id="tmpl">
  <style>
:host([centered]) {
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      border-color: blue;
    }

    :host {
      display: inline-block;
      border: 1px solid red;
      padding: 10px;
    }
  </style>
  <slot></slot>
</template>

<script>
customElements.define('custom-dialog', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
  }
});
</script>


<custom-dialog centered>
  Centered!
</custom-dialog>

<custom-dialog>
  Not centered.
</custom-dialog>

现在附加的居中样式只应用于第一个对话框:<custom-dialog centered>

:host-context(selector)

与 :host 相同,但仅当外部文档中的 shadow 宿主或它的任何祖先节点与 selector 匹配时才应用样式。

例如,:host-context(.dark-theme) 只有在 <custom-dialog> 或者 <custom-dialog> 的任何祖先节点上有 dark-theme 类时才匹配:

<body class="dark-theme">
  <!--
    :host-context(.dark-theme) 只应用于 .dark-theme 内部的 custom-dialog
  -->
  <custom-dialog>...</custom-dialog>
</body>

总之,我们可以使用 :host-family 系列的选择器来对组件的主元素进行样式设置,具体取决于上下文。这些样式(除 !important 外)可以被文档样式覆盖。

给占槽( slotted )内容添加样式

现在让我们考虑有插槽的情况。

占槽元素来自 light DOM,所以它们使用文档样式。局部样式不会影响占槽内容。

在下面的例子中,按照文档样式,占槽的 <span> 是粗体,但是它不从局部样式中获取 background

<style>
span { font-weight: bold }
</style>

<user-card>
  <div slot="username"> < span > John Smith</span> </div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
span { background: red; }
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

结果是粗体,但不是红色。

如果我们想要在我们的组件中设置占槽元素的样式,有两种选择。

首先,我们可以对 <slot> 本身进行样式化,并借助 CSS 继承:

<user-card>
  <div slot="username"> < span > John Smith</span> </div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
slot[name="username"] { font-weight: bold; }
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

这里 <p>John Smith</p> 变成粗体,因为 CSS 继承在 <slot> 和它的内容之间有效。但是在 CSS 中,并不是所有的属性都是继承的。

另一个选项是使用 ::slotted(selector) 伪类。它根据两个条件来匹配元素:

  1. 这是一个占槽元素,来自于 light DOM。插槽名并不重要,任何占槽元素都可以,但只能是元素本身,而不是它的子元素 。
  2. 该元素与 selector 匹配。

在我们的例子中,::slotted(div) 正好选择了 <div slot="username"> ,但是没有选择它的子元素:

<user-card>
  <div slot="username">
    <div>John Smith</div>
  </div>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <style>
::slotted(div) { border: 1px solid red; }
      </style>
      Name: <slot name="username"></slot>
    `;
  }
});
</script>

请注意,::slotted 选择器不能用于任何插槽中更深层的内容。下面这些选择器是无效的:

::slotted(div span) {
  /* 我们插入的 <div> 不会匹配这个选择器 */
}

::slotted(div) p {
  /* 不能进入 light DOM 中选择元素 */
}

此外,::sloated 只能在 CSS 中使用,不能在 querySelector 中使用。