在本系列关于 web 组件的 持续文章中,我们讨论了很多关于在 web 组件中使用 CSS 的内部细节,但也有一些特殊的伪元素和伪类,它们就像好朋友一样,在你与潜在爱人交谈之前,会乐意闻闻你可能不太好的呼吸。 你知道,他们在你需要的时候帮助你。 而且,就像一位好朋友会递给你一颗薄荷糖一样,这些伪元素和伪类从组件内部和组件外部(即 web 组件所在的网站)都为你提供了一些解决方案。
我特指 ::part
和 ::slotted
伪元素,以及 :defined
、:host
和 :host-context
伪类。 它们为我们提供了更多与 web 组件交互的方式。 让我们更仔细地研究一下它们。
文章系列
- Web 组件比你想象的更容易
- 交互式 Web 组件比你想象的更容易
- 在 WordPress 中使用 Web 组件比你想象的更容易
- 用 Web 组件增强内置元素“是”比你想象的更容易
- 上下文感知 Web 组件比你想象的更容易
- 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.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2022/02/s_3F18A0CD3F6686952ADF5360C5603A8E3FDF98E048CFA91F2B2AE1FE647BF904_1643999519141_host-pseudo-dev-tools-scaled.jpg?resize=2560%2C549&ssl=1)
也就是说,: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
?请在评论中告诉我。(或者,如果你有治疗慢性口臭的方法,我的妻子会很乐意听到更多。)
不错的总结!
悬停动画的示例在我的尝试过的所有浏览器(Chrome、Chrome Canary、Firefox、Edge)中似乎都不起作用。无法确定是打字错误,还是还没有提供?
抱歉。我绝不会想到,但看起来
:hover
ing 部分只在 Safari 中起作用。*耸肩*我已经相应地更新了文章。看起来
:host-context
在 Safari 和 Firefox 上还不可用 (https://caniuse.cn/?search=%3Ahost-context)。太可惜了,它看起来非常有用!