2021 年 5 月 17 日

Shadow DOM 样式

Shadow DOM 可以包含 <style><link rel="stylesheet" href="…"> 标签。在后一种情况下,样式表是 HTTP 缓存的,因此不会为使用相同模板的多个组件重新下载。

一般来说,本地样式只在 shadow tree 内起作用,而文档样式在 shadow tree 外起作用。但有一些例外。

:host

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

例如,我们正在制作一个应该居中的<custom-dialog>元素。为此,我们需要对<custom-dialog>元素本身进行样式设置。

这正是:host的作用。

<template id="tmpl">
  <style>
    /* the style will be applied from inside to the custom-dialog element */
    :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>

级联

影子宿主(<custom-dialog>本身)位于光 DOM 中,因此会受到文档 CSS 规则的影响。

如果某个属性在:host中本地设置了样式,并在文档中也设置了样式,那么文档样式将优先。

例如,如果在文档中我们有

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

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

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

例外情况是,当本地属性被标记为!important时,对于此类属性,本地样式将优先。

:host(selector)

:host相同,但仅在影子宿主匹配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系列选择器来设置组件主元素的样式。这些样式(除非!important)可以被文档覆盖。

设置插槽内容的样式

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

插槽元素来自光 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. 这是一个来自光 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) {
  /* our slotted <div> does not match this */
}

::slotted(div) p {
  /* can't go inside light DOM */
}

此外,::slotted 只能在 CSS 中使用。我们无法在querySelector中使用它。

使用自定义属性的 CSS 钩子

我们如何从主文档中为组件的内部元素设置样式?

:host这样的选择器会将规则应用于<custom-dialog> 元素或<user-card>,但如何为它们内部的 Shadow DOM 元素设置样式呢?

没有选择器可以从文档中直接影响 Shadow DOM 样式。但就像我们公开方法来与我们的组件交互一样,我们可以公开 CSS 变量(自定义 CSS 属性)来设置其样式。

自定义 CSS 属性存在于所有级别,包括光 DOM 和 Shadow DOM。

例如,在 Shadow DOM 中,我们可以使用--user-card-field-color CSS 变量来设置字段的样式,而外部文档可以设置其值。

<style>
  .field {
    color: var(--user-card-field-color, black);
    /* if --user-card-field-color is not defined, use black color */
  }
</style>
<div class="field">Name: <slot name="username"></slot></div>
<div class="field">Birthday: <slot name="birthday"></slot></div>

然后,我们可以在外部文档中为<user-card>声明此属性。

user-card {
  --user-card-field-color: green;
}

自定义 CSS 属性可以穿透 Shadow DOM,它们在任何地方都可见,因此内部的.field 规则将使用它。

以下是完整的示例。

<style>
  user-card {
    --user-card-field-color: green;
  }
</style>

<template id="tmpl">
  <style>
    .field {
      color: var(--user-card-field-color, black);
    }
  </style>
  <div class="field">Name: <slot name="username"></slot></div>
  <div class="field">Birthday: <slot name="birthday"></slot></div>
</template>

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

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

总结

Shadow DOM 可以包含样式,例如<style><link rel="stylesheet">

本地样式可以影响

  • Shadow DOM 树,
  • 使用:host:host() 伪类的 Shadow 宿主,
  • 插槽元素(来自光 DOM),::slotted(selector) 允许选择插槽元素本身,但不包括其子元素。

文档样式可以影响

  • Shadow 宿主(因为它位于外部文档中)
  • 插槽元素及其内容(因为它们也位于外部文档中)

当 CSS 属性冲突时,通常文档样式具有优先级,除非该属性被标记为!important。然后本地样式具有优先级。

CSS 自定义属性可以穿透 Shadow DOM。它们被用作“钩子”来设置组件的样式。

  1. 组件使用自定义 CSS 属性来设置关键元素的样式,例如var(--component-name-title, <default value>)
  2. 组件作者为开发人员发布这些属性,它们与其他公共组件方法一样重要。
  3. 当开发者想要为标题设置样式时,他们会为影子宿主或其上层元素分配--component-name-title CSS 属性。
  4. 盈利!
教程地图

评论

在评论之前阅读…
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用<code> 标签,对于多行代码,请将其包装在<pre> 标签中,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)