了解如何使用 @scope 仅选择 DOM 的有限子树中的元素。
浏览器支持
-
<ph type="x-smartling-placeholder">
-
<ph type="x-smartling-placeholder">
-
-
<ph type="x-smartling-placeholder">
编写 CSS 选择器的精细技巧
编写选择器时,您可能会发现自己在两种情况之间苦苦挣扎。一方面,您需要非常具体地选择哪些元素。另一方面,您希望选择器始终易于替换,并且不要与 DOM 结构紧密耦合。
例如,如果您要选择“卡片组件内容区域中的主打图片”(这是非常具体的元素选择),那么您很可能不希望编写 .card > .content > img.hero
这样的选择器。
- 此选择器具有非常高的
(0,3,1)
特异性,因此随着代码内容的增加,很难进行替换。 - 通过依赖直接子组合器,它与 DOM 结构紧密耦合。如果标记发生变化,您还需要更改 CSS。
但是,您也不要只编写 img
作为该元素的选择器,因为这样会选择页面中的所有图片元素。
在这方面找到适当的平衡通常是一项巨大的挑战。多年来,一些开发者提出了解决方案和解决方法,以帮助您应对此类情况。例如:
- BEM 等方法要求您为该元素指定
card__img card__img--hero
类,以保持较低的特异性,同时让您可以具体选择内容。 - 基于 JavaScript 的解决方案(如限定范围的 CSS 或样式化组件)通过向选择器添加随机生成的字符串(如
sc-596d7e0e-4
)来重写所有选择器,以防止它们定位到网页另一侧的元素。 - 有些库甚至完全废弃了选择器,并要求您将样式设置触发器直接放入标记本身。
但如果您根本不需要这些标签呢?如果 CSS 让您既能非常具体地指定所选元素,又无需编写特异性较强的选择器或与 DOM 紧密耦合的选择器,那该有多好?这正是 @scope
大显身手的地方,它提供了一种仅在 DOM 子树内选择元素的方法。
@scope 简介
借助 @scope
,您可以限制选择器的覆盖面。为此,您可以设置范围根,确定要定位的子树的上限。有了作用域根集,所包含的样式规则(称为作用域样式规则)只能从 DOM 的这一受限子树中进行选择。
例如,若要仅定位到 .card
组件中的 <img>
元素,可将 .card
设置为 @scope
at-rule 的范围根。
@scope (.card) {
img {
border-color: green;
}
}
限定了范围的样式规则“img { … }
”只能有效地选择在所匹配 .card
元素的范围内的 <img>
元素。
为防止选择卡片内容区域 (.card__content
) 内的 <img>
元素,您可以让 img
选择器选择更具体的对象。另一种方法是利用 @scope
at-rule 也接受用于确定下限的范围限制这一事实。
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
此限定范围的样式规则仅定位到位于祖先树中 .card
和 .card__content
元素之间的 <img>
元素。这种具有上限和下限的范围通常称为“甜甜圈范围”。
:scope
选择器
默认情况下,所有限定了范围的样式规则都与范围根相关。您也可以定位限定范围的根元素本身。为此,请使用 :scope
选择器。
@scope (.card) {
:scope {
/* Selects the matched .card itself */
}
img {
/* Selects img elements that are a child of .card */
}
}
限定作用域的样式规则内的选择器会隐式添加 :scope
前缀。如果需要,您可以通过自行添加 :scope
前缀来明确指出这一点。或者,您也可以在 CSS 嵌套中附加 &
选择器。
@scope (.card) {
img {
/* Selects img elements that are a child of .card */
}
:scope img {
/* Also selects img elements that are a child of .card */
}
& img {
/* Also selects img elements that are a child of .card */
}
}
范围限制可以使用 :scope
伪类来要求与范围根存在特定关系:
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
范围限制还可以使用 :scope
来引用其范围根之外的元素。例如:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
请注意,作用域样式规则本身无法转义子树。诸如 :scope + p
之类的选择无效,因为它会尝试选择不在范围内的元素。
@scope
和特异性
您在 @scope
序言中使用的选择器不会影响所含选择器的特异性。在以下示例中,img
选择器的特异性仍然是 (0,0,1)
。
@scope (#sidebar) {
img { /* Specificity = (0,0,1) */
…
}
}
:scope
的特异性是常规伪类(即 (0,1,0)
)的特异性。
@scope (#sidebar) {
:scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
…
}
}
在以下示例中,&
在内部被重写到用于范围根使用的选择器,并封装在 :is()
选择器中。最后,浏览器将使用 :is(#sidebar, .card) img
作为选择器来执行匹配。此过程称为脱糖。
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
…
}
}
由于 &
使用 :is()
进行脱糖,因此 &
的特异性是根据 :is()
特异性规则计算的:&
的特异性是其最具体的参数。
在此示例中,:is(#sidebar, .card)
的特异性是其最具体的参数(即 #sidebar
),因此会变为 (1,0,0)
。结合 img
的特异性(即 (0,0,1)
),您最终得到的就是 (1,0,1)
作为整个复杂选择器的特异性。
@scope (#sidebar, .card) {
& img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
…
}
}
@scope
内 :scope
和 &
之间的区别
除了计算特异性的方式存在差异之外,:scope
和 &
之间的另一个差异是,:scope
表示匹配的范围根,而 &
表示用于匹配范围根的选择器。
因此,您可以多次使用 &
。这与 :scope
不同,后者只能使用一次,因为您无法匹配范围根中的范围根。
@scope (.card) {
& & { /* Selects a `.card` in the matched root .card */
}
:scope :scope { /* ❌ Does not work */
…
}
}
无前序范围
使用 <style>
元素编写内嵌样式时,您可以通过不指定任何范围根,将样式规则的作用域限定为 <style>
元素的所属父元素。可以省略 @scope
的序言。
<div class="card">
<div class="card__header">
<style>
@scope {
img {
border-color: green;
}
}
</style>
<h1>Card Title</h1>
<img src="…" height="32" class="hero">
</div>
<div class="card__content">
<p><img src="…" height="32"></p>
</div>
</div>
在上面的示例中,限定了范围的规则仅定位到 div
中类名称为 card__header
的元素,因为该 div
是 <style>
元素的父元素。
级联中的 @scope
在 CSS 级联广告素材内,@scope
还添加了一个新条件:作用域邻近性。这一步在具体程度和出现顺序之前。
根据规范:
比较出现在具有不同范围根的样式规则中的声明时,在范围根和限定范围的样式规则主题之间跳转次数最少的声明胜出。
嵌套组件的多个变体时,这个新步骤会派上用场。例如,此示例尚未使用 @scope
:
<style>
.light { background: #ccc; }
.dark { background: #333; }
.light a { color: black; }
.dark a { color: white; }
</style>
<div class="light">
<p><a href="#">What color am I?</a></p>
<div class="dark">
<p><a href="#">What about me?</a></p>
<div class="light">
<p><a href="#">Am I the same as the first?</a></p>
</div>
</div>
</div>
查看这小段标记时,第三个链接将为 white
,而不是 black
,即使它是 div
的子级且应用了 .light
类。这取决于出现顺序,级联在此处根据该标准确定胜出者。它看到最后声明了 .dark a
,因此它将胜过 .light a
规则
现在,借助限定了范围的邻近性条件,此问题解决了:
@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}
@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}
由于两个限定范围的 a
选择器的特异性相同,因此限定范围的邻近性条件会发挥作用。它会按与范围根的邻近程度来权衡两个选择器的权重。对于第三个 a
元素,它只是到 .light
作用域根的一次跳,但两次跳到 .dark
的跳跃。因此,.light
中的 a
选择器将胜出。
结束语:选择器隔离,而非样式隔离
需要注意的一点是,@scope
会限制选择器的覆盖范围,而不会提供样式隔离。向下沿用到子级的属性仍会超出 @scope
的下限。color
就是这样一种属性。在甜甜圈作用域内声明该值时,color
仍会向下继承给甜甜圈孔内的子项。
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
在上面的示例中,.card__content
元素及其子项的颜色为 hotpink
,因为它们从 .card
继承了值。
(封面照片:rustam burkhanov,在 Un 划定的平台上)