Web 组件伪类和伪元素比你想象的更容易

Avatar of John Rhea
John Rhea

DigitalOcean 提供适合旅程各个阶段的云产品。 立即开始使用 200 美元的免费积分!

在本系列关于 web 组件的 持续文章中,我们讨论了很多关于在 web 组件中使用 CSS 的内部细节,但也有一些特殊的伪元素和伪类,它们就像好朋友一样,在你与潜在爱人交谈之前,会乐意闻闻你可能不太好的呼吸。 你知道,他们在你需要的时候帮助你。 而且,就像一位好朋友会递给你一颗薄荷糖一样,这些伪元素和伪类从组件内部组件外部(即 web 组件所在的网站)都为你提供了一些解决方案。

我特指 ::part::slotted 伪元素,以及 :defined:host:host-context 伪类。 它们为我们提供了更多与 web 组件交互的方式。 让我们更仔细地研究一下它们。

文章系列

::part 伪元素

简而言之,::part 允许你穿透影子树,这只是我用《指环王》的方式来说,它允许你从影子 DOM 外部影子 DOM 内部的元素进行样式设置。 理论上,你应该将影子 DOM 的所有样式封装在影子 DOM 内,即封装在 <template> 元素中的 <style> 元素内。

因此,假设在 本系列的第一部分中,你有一个 <h2> 元素在你的 <template> 元素内,那么该 <h2> 元素的所有样式都应该在 <style> 元素内。

<template id="zprofiletemplate">
  <style>
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    /* other styles */
  </style>
  <div class="profile-wrapper">
    <div class="info">
      <h2>
        <slot name="zombie-name">Zombie Bob</slot>
      </h2>
      <!-- other zombie profile info -->
    </div>
</template>

但是,有时我们可能需要根据页面上存在的信息来设置影子 DOM 内元素的样式。 例如,假设我们为不死之爱系统中每个具有匹配项的僵尸创建一个页面。 我们可以根据匹配程度,向个人资料添加一个类。 然后,例如,我们可以突出显示匹配者的姓名,如果他/她/它是一个很好的匹配。 匹配的密切程度将根据显示的潜在匹配列表而有所不同,在进入该页面之前我们不会知道该信息,因此无法将该功能嵌入到 web 组件中。 然而,由于 <h2> 元素在影子 DOM 内,因此我们无法从影子 DOM 外部访问或设置其样式,这意味着匹配页面上的 zombie-profile h2 选择器将不起作用。

但是,如果我们通过向 <h2> 元素添加一个 part 属性,对 <template> 标记进行一些调整

<template id="zprofiletemplate">
  <style>
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    /* other styles */
  </style>
  <div class="profile-wrapper">
    <div class="info">
      <h2 part="zname">
        <slot name="zombie-name">Zombie Bob</slot>
      </h2>
      <!-- other zombie profile info -->
    </div>
</template>

就像在嘴里喷了一点碧昂丝,我们现在拥有了穿透影子 DOM 障碍的能力,并能够从外部 <template> 设置这些元素的样式

/* External stylesheet */
.high-match::part(zname) {
  color: blue;
}
.medium-match::part(zname) {
  color: navy;
}
.low-match::part(zname) {
  color: slategray;
}

在使用 CSS ::part 时,有很多因素需要考虑。 例如,设置 part 内部元素的样式是不允许的

/* frowny-face emoji */
.high-match::part(zname) span { ... }

但是你可以在该元素上添加一个 part 属性,并通过其自己的 part 名称对其进行样式设置。

但是,如果我们有一个 web 组件嵌套在另一个 web 组件内,会发生什么情况? ::part 仍然有效吗? 如果 web 组件出现在页面的标记中,即你将其插入,那么 ::part 可以从主页面上的 CSS 正常工作。

<zombie-profile class="high-match">
  <img slot="profile-image" src="https://assets.codepen.io/1804713/leroy.png" />
  <span slot="zombie-name">Leroy</span>
  <zombie-details slot="zdetails">
    <!-- Leroy's details -->
  </zombie-details>
</zombie-profile>

但是如果 web 组件在模板/影子 DOM 中,那么 ::part 无法穿透两个影子树,只能穿透第一个。 我们需要将 ::part 放到光明中…… 这么说吧。 我们可以使用 exportparts 属性做到这一点。

为了演示这一点,我们将使用 web 组件在个人资料后面添加一个“水印”。(为什么? 相信我,这是我能想到的最小程度的例子。) 以下是我们的模板:(1)<zombie-watermark> 的模板,以及(2)<zombie-profile> 的相同模板,但在末尾添加了一个 <zombie-watermark> 元素。

<template id="zwatermarktemplate">
  <style>
    div {
    text-transform: uppercase;
      font-size: 2.1em;
      color: rgb(0 0 0 / 0.1);
      line-height: 0.75;
      letter-spacing: -5px;
    }
    span {
      color: rgb( 255 0 0 / 0.15);
    }
  </style>
  <div part="watermark">
    U n d y i n g  L o v e  U n d y i n g  L o v e  U n d y i n g  L o v e  <span part="copyright">©2 0 2 7 U n d y i n g  L o v e  U n L t d .</span>
  <!-- Repeat this a bunch of times so we can cover the background of the profile -->
  </div> 
</template>
<template id="zprofiletemplate">
  <style>
    ::part(watermark) {
      color: rgb( 0 0 255 / 0.1);
    }
    /* More styles */
  </style>
  <!-- zombie-profile markup -->
  <zombie-watermark exportparts="copyright"></zombie-watermark>
</template>
<style>
  /* External styles */
  ::part(copyright) {
    color: rgb( 0 100 0 / 0.125);
  }
</style>

由于 ::part(watermark) 只有一个影子 DOM 位于 <zombie-watermark> 上方,因此它在 <zombie-profile> 的模板样式中可以正常工作。 此外,由于我们在 <zombie-watermark> 上使用了 exportparts="copyright",因此 copyright part 已被推送到 <zombie-profile> 的影子 DOM 中,::part(copyright) 现在即使在外部样式中也可以正常工作,但 ::part(watermark)<zombie-profile> 的模板外部将无法工作。

我们还可以使用该属性转发并重命名部分

<zombie-watermark exportparts="copyright: cpyear"></zombie-watermark>
/* Within zombie-profile's shadow DOM */

/* happy-face emoji */
::part(cpyear) { ... }

/* frowny-face emoji */
::part(copyright) { ... }

结构性伪类(:nth-child 等)也不适用于部分,但至少在 Safari 中,你可以使用 :hover 等伪类。 让我们稍微动画化高匹配名称,让他们在寻找爱情时摇晃一下。 好吧,我听到了,也同意这很尴尬。 让我们…… 呃…… 使它们更加,应该说,引人注目,并添加一些动作。

.high::part(name):hover {
  animation: highmatch 1s ease-in-out;
}

::slotted 伪元素

当我们介绍交互式 web 组件时,实际上就提到了 ::slotted CSS 伪元素。 基本思想是,::slotted 代表 web 组件中 slot 中的任何内容,即具有 slot 属性的元素。 但是,::part 会穿透影子 DOM,使 web 组件的元素可供外部样式访问,而 ::slotted 则保持封装在组件 <template> 中的 <style> 元素中,并访问实际上位于影子 DOM 外部的元素。

例如,在我们的 <zombie-profile> 组件中,每个个人资料图片都通过 slot="profile-image" 插入到元素中。

<zombie-profile>
  <img slot="profile-image" src="photo.jpg" /> 
  <!-- rest of the content -->
</zombie-profile>

这意味着我们可以像这样访问该图片——以及任何其他插槽中的任何图片

::slotted(img) {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}

类似地,我们可以使用 ::slotted(*) 选择所有插槽,而不管它是什么元素。 只需要注意的是,::slotted 必须选择一个元素——文本节点不受 ::slotted 僵尸样式的影响。 并且插槽中元素的子元素不可访问。

:defined 伪类

:defined 匹配所有已定义的元素(我知道,令人惊讶,对吧?),包括内置元素和自定义元素。 如果你的自定义元素像僵尸一样四处游荡,躲避女朋友的爸爸关于他“居住”情况的问题,你可能不希望在等待内容“复活”时显示内容的残骸…… 呃…… 加载。

你可以使用 :defined 伪类在 web 组件可用之前(或“定义”之前)将其隐藏,例如

:not(:defined) {
  display: none;
}

你可以看到 :defined 如何充当组件样式中的薄荷糖,防止任何损坏的内容显示(或口臭泄漏),同时页面仍在加载。 一旦元素被定义,它就会自动出现,因为它现在,你知道,被定义了,而不是定义。

我在以下演示中向 web 组件添加了 5 秒的 setTimeout。 这样,你可以看到 <zombie-profile> 元素在未定义时不会显示。 包含 <zombie-profile> 组件的 <h1><div> 仍然存在。 只是 <zombie-profile> web 组件被设置为 display: none,因为它们尚未定义。

:host 伪类

假设你想要对自定义元素本身进行样式更改。 虽然你可以从自定义元素外部进行此操作(就像收紧 N95 口罩一样),但结果不会被封装,并且必须将其他 CSS 传输到放置此自定义元素的任何地方。

因此,拥有一个能够到达影子 DOM 外部并选择影子根的伪类将会非常方便。 该 CSS 伪类是 :host

在本系列前面的示例中,我从主页面上的 CSS 设置了 <zombie-profile> 的宽度,例如

zombie-profile {
  width: calc(50% - 1em);
}

但是,使用 :host,我可以从web 组件内部设置该宽度,例如

:host {
  width: calc(50% - 1em);
}

事实上,在我的示例中有一个类为.profile-wrapper的 div,现在我可以将其移除,因为我可以使用:host将 shadow root 作为我的包装器。这是一种简化标记的好方法。

你可以从:host中进行后代选择器,但只能访问 shadow DOM 中的后代——任何被插入到你的 Web 组件中的内容(不使用::slotted)。

Showing the parts of the HTML that are relevant to the :host pseudo-element.

也就是说,:host不是一个一招致胜的僵尸。它还可以接受参数,例如类选择器,并且只有在存在该类的情况下才会应用样式。

:host(.high) {
  border: 2px solid blue;
}

这允许你在向自定义元素添加某些类时进行更改。

你也可以在其中传递伪类,例如:host(:last-child):host(:hover)

:host-context伪类

现在让我们谈谈:host-context。它就像我们的朋友:host(),但更强大。虽然:host可以获取到 shadow root,但它不会告诉你自定义元素所在的上下文,或者它的父元素和祖先元素。

另一方面,:host-context打破了限制,允许你沿着 DOM 树向上追踪到彩虹尽头的独角兽。请注意,在我写这篇文章的时候,:host-context在 Firefox 或 Safari 中不受支持。因此,将其用于渐进增强。

以下是它的工作原理。我们将把我们的僵尸档案列表分成两个 div。第一个 div 将包含所有具有.bestmatch类的最佳匹配僵尸。第二个 div 将包含所有中等和低匹配的僵尸,其类为.worstmatch

<div class="profiles bestmatch">
  <zombie-profile class="high">
    <!-- etc. -->
  </zombie-profile>
  <!-- more profiles -->
</div>

<div class="profiles worstmatch">
  <zombie-profile class="medium">
    <!-- etc. -->
  </zombie-profile>
  <zombie-profile class="low">
    <!-- etc. -->
  </zombie-profile>
  <!-- more profiles -->
</div>

假设我们希望对.bestmatch.worstmatch类应用不同的背景颜色。我们无法仅使用:host来实现这一点。

:host(.bestmatch) {
  background-color: #eef;
}
:host(.worstmatch) {
  background-color: #ddd;
}

这是因为我们的最佳匹配和最差匹配类不在我们的自定义元素上。我们想要的是能够从 shadow DOM 中选择档案的父元素。:host-context会越过自定义元素,以匹配我们想要设置样式的匹配类。

:host-context(.bestmatch) {
  background-color: #eef;
}
:host-context(.worstmatch) {
  background-color: #ddd;
}

好吧,感谢你尽管有口臭,还是坚持了下来。(我知道你没有感觉到,但上面我谈论你的口臭时,其实是在偷偷地谈论我的口臭。)

你将在你的 Web 组件中如何使用::part::slotted:defined:host:host-context?请在评论中告诉我。(或者,如果你有治疗慢性口臭的方法,我的妻子会很乐意听到更多。)