通过浏览器原生支持的 CSS
嵌套,可以大幅减少编写的CSS
代码量,同时使得代码更易于维护和理解。
CSS
嵌套是CSS
的一种新语法,允许将选择器嵌套在其他选择器内,其中每个嵌套的选择器都相对于其父级。
它的最简单形式如下:
1 | .card { |
浏览器会将应用于具有类名为card-title
的元素的样式仅限于该元素嵌套在具有类名为card
的元素内。换句话说,浏览器会按照以下方式翻译:
1 | .card { |
原生CSS嵌套的好处
减少输入
在上述“翻译”版本中,您可以看到浏览器为我们复制了.card
,但在我们的源代码中,我们只需要写一次。通过嵌套选择器,如果要防止样式应用得太广泛,您就不再需要重复选择器的一部分。此外,减少输入还意味着您的文件最终会更小,因此下载速度更快。
更容易进行范围限定
这也意味着更容易进行范围限定。如果您正在处理卡片,您只需写一次.card
选择器,并在其中嵌套所有相关的CSS
。这意味着您需要编写较少特定的选择器,因为它们已经作用于其父元素。这还使您的代码更具可移植性,因为您不必浏览CSS以找到应用于卡片的所有样式,而可以直接使用.card
和其中的所有内容。
CSS
遵循DOM
结构
这样做的好处是,您的CSS
开始模仿您的DOM
。在上面的例子中,card-title
在CSS
中和在DOM
中都是嵌套的。当您的CSS模仿您的DOM时,更容易理解和理解更改的效果。如果您曾经在大型CSS
代码库上工作过,您会知道即使您认为不再使用CSS
,删除CSS
也是多么危险。
当您的CSS
遵循HTML
结构时,您可以更加自信地删除对象。例如,当您从卡片HTML
中删除card-footer
时,您知道您的.card-footer CSS
只能应用于站点的该部分,因此您也可以从CSS
中将其删除。
嵌套的复杂性
现在您了解了CSS嵌套的有用之处,让我们来探讨一些复杂性。
浏览器支持
此刻我们需要指出,目前有两个版本的CSS嵌套:严格版本,即Chromium
版本到119和Safari
版本到17.1实现的版本,以及“宽松”版本,即Firefox
随附的版本,并且现在在Safari 17.2
和Chromium
120中也可用。
它们之间的区别在于它们如何处理嵌套的元素选择器(如p、div
)。严格版本允许任何以非字母开头的选择器在没有“嵌套选择器”&
的情况下嵌套。这意味着不需要这样:
1 | .card { |
我们不得不写成下面这样
1 | .card { |
否则,您的h3
样式将不会被应用。其原因在于属性(例如margin
)也以字母开头,因此浏览器的CSS
引擎无法确定您是在添加新属性还是新的嵌套选择器。
浏览器找到了一种解决方法,因此宽松版本允许您在没有“&”
的情况下嵌套元素选择器,使一切变得更加简单。到您阅读本文时,Chromium
120和Safari 17.2
将已发布,因此所有浏览器现在都支持宽松版本。
建议您继续使用严格版本,因为这将在所有浏览器上保持可用。
嵌套选择器及浏览器解析嵌套的方式
那么,这是否意味着您不再需要嵌套选择器呢?对于简单的嵌套来说,不再需要。但是嵌套选择器使您能够控制嵌套的方式。为了理解它是如何实现的,您需要了解三件事:
- 如果嵌套选择器没有
“&”
,浏览器会在其前面(带有空格)隐式地添加一个 - 在
CSS
引擎中,“&”
被替换为“is(<parent selector>)”
- 您可以在选择器的任何位置添加
“&”
(包括多次)
让我们按顺序详细了解这些事项。
隐式嵌套选择器
回到我们的第一个例子,嵌套的.card-title
没有嵌套选择器,因此浏览器会隐式地添加一个:
1 | .card { |
单独来看,这没什么大不了的,但在了解到“&”
会被替换时,这一点很重要。
解析的选择器
“&”
被替换为“is(<parent selector>)”
,因此上述代码中的嵌套选择器最终不是如本文开头所说的 .card .card-title
,而是 :is(.card) .card-title
。
在这个例子中,这没什么大不了的,因为它们都具有相同的权重(0,2,0),但如果您的父选择器是以逗号分隔的选择器,您可能会意外地制定一个特异性非常高的权重:
1 | .card, |
这将被解析为 :is(.card, #xmas-card) .card-title
,其权重为 (1,1,0),因为:is()
伪选择器将获得最特定项的权重,即在这种情况下是 id
。如果您的 .card-title
完全不在 #xmas-card
中,而是在常规的 .card
中,情况也是如此。
如果您意识到 :is()
选择器也会被嵌套,情况可能变得更加复杂。让我们看看在以下三层深度嵌套中是如何工作的:
1 | .card { |
首先,“p”
是隐式的 & p
。这将替换为父级,为了方便起见,我们也会添加 &
,创建 :is(& .card-body) p
。最后,我们还在那里填入 "&"
,用:is(.card)
替换它,结果是 :is(:is(.card) .card-body) p
,这是您的浏览器最终使用的内容。
添加 & 选择器
您可以通过添加 &
选择器来明确嵌套方式。我个人认为这样更易读,而且除此之外,它还会影响嵌套的工作方式。这是因为空格也是一种选择器:后代选择器。因此,您是否在 &
后面添加空格可能会有所不同。
以下代码将在悬停卡片时应用:
1 | .card { |
这是因为它解析为 :is(.card):hover
。
以下代码将产生不同的效果:
1 | .card { |
由于存在空格,:hover
部分现在是在卡片的子节点上生效,因此它的作用类似于 :is(.card) *:hover
。伪类如:hover
在它们之前会有一个隐式的 *
(通用元素选择器)。所以请记住这个空格。
通过在选择器的其他地方添加显式的 &
选择器,你可以针对嵌套的不同部分进行定位。例如,要为卡片添加样式,但仅当它们在列表中时,可以这样做:
1 | .list { |
不利之处是你的卡片组件的所有样式也将具有 .list 作为父选择器,使其更具体且不够模块化。
相反,你可以这样写:
1 | .card { |
这样,所有其他卡片样式都可以被限定在卡片内,当卡片在列表中时的额外样式也会随之包含,保持整体简洁而模块化。
添加多个选择器在你想要按顺序样式化元素时非常有用。例如,在列表中为每个卡片添加边距,除了第一个:
1 | .card { |
嵌套@规则
CSS嵌套的一个非常有用的部分是,你还可以嵌套诸如@media、@supports
等规则。
与其编写以下代码:
1 | .card { |
你可以嵌入media query
1 | .card { |
注意我们无需重复.card
,而且我们可以直接在媒体查询中使用CSS
声明,因为浏览器会隐式地将其解读为:
1 | .card { |
而且正如我们之前学到的那样,该&
然后被替换为:is(.card)
。
注意事项
当我第一次使用Sass
时,我对嵌套情有独钟,有时嵌套了20层深。在构建样式时,一直进行下去确实很诱人。但是每一层的嵌套也会增加选择器的特异性,使得以后编辑变得更加困难(更不用说缩进了!)。
一个简单的经验法则是,当你达到3或4层深度时,看看是否可以进入新的嵌套集。这样可以使您的选择器更容易理解,并且可能存在组件及其子组件的逻辑分隔点。有多种其他应对此情况的策略,比如保留空的父选择器,以及在布局和样式之间进行拆分。
另一个棘手的部分是,由于嵌套被解析为非嵌套代码,您编写CSS
的顺序可能不是CSS应用的顺序。简而言之:您可以在应用常规属性之前嵌套@规则,如下所示:
1 | body { |
如果按照CSS
的顺序进行,背景将是蓝色,因为@
规则不会添加特异性,而background: blue
最后出现。
一旦浏览器完成计算CSS
值,它最终应用规则如下,并将媒体查询移到最后:
1 | body { |
现在,background: red
是最后一个,最终成为应用的背景颜色。
处理这个问题,保持代码易于理解的最佳方式是仅在所有常规属性之后嵌套,即使嵌套语法允许混合它们。
总结
CSS嵌套在所有浏览器中都可用,尽管为了保持最广泛的兼容性,您可能希望继续使用严格的表示法。
嵌套有助于使您的CSS更小,更易于理解,并更紧密地遵循您正在样式化的DOM。在使用嵌套时,请尽量避免嵌套太深,并记住您的CSS需要被解析,这可能会改变浏览器最终使用的顺序和选择器。