图解CSS:CSS的嵌套

发布于 大漠

对于众多 CSSer 来说,多年来一直希望在原生的 CSS 有类似变量,嵌套和函数等特性。虽然近些年,CSS 得到快速的发展,比如说,大家期望的 CSS 变量已经可以直接在 CSS中使用了CSS也具备一些函数计算(甚至简单的逻辑判断)能力。今天要告诉大家的是,CSS 原生的嵌套很快就会出现在浏览器上。在今天这篇文章中就将和大家一起来聊聊 CSS 中原生的嵌套,以及如何使用它,在使用的过程需要怎样避免一些陷阱。

先从CSS处理器说起

十多年前,也就大约是2010年左右,CSS处理器开始被广大CSSer或Web开发者青眯,主要是因为这些CSS处理器(比如Sass、LESS、Stylus或PostCSS等)具备一些非常优秀的特性,就拿Sass这个CSS处理器来说,它具备变量,嵌套、混合宏和扩展器等。

Sass中的变量在过去是非常有用的,但现在逐步被原生的CSS自定义属性(也就是CSS变量)所替代。CSS原生的变量到今天能在众多主流浏览器上使用,成为规范的模块之一也是花了很长时间的。作为 CSS 语言的一部分,CSS变量比Sass这样的处理器的变量要强大得多,而且还可以使用 JavaScript来修改。

因此,对于Web开发者而言,特别是CSSer,他们得到了CSS原生变量带来的优惠,但同时也期待着CSS的原生嵌套能成为一模块,并且得到浏览器支持。

众所周之,CSS的嵌套能给开发得带来便利,让代码更易于维护,比如下面这段SCSS代码:

$M: 640px;
$color-grey-33: #333;

.key-Selector {
    width: 100%;
    
    @media (min-width: $M) {
        width: 50%;
    }

    .an-Override_Selector & {
        color: $color-grey-33;
    }
}

经过编译之后:

.key-Selector {
    width: 100%;
}

@media (min-width: 640px) {
    .key-Selector {
        width: 50%;
    }
}

.an-Override_Selector .key-Selector {
    color: #333;
}

虽然像Sass这样的处理器能给我们带来很多便利,但它并不能直接运行在客户端上(比如浏览器),它需要经过一些编译处理:

可能还会采用一些其他处理器(大家常称的后处理器)处理编译出来的CSS代码:

最终编译处理好的代码才能被客户端识别解析。

而原生的就不一样了,他不需要任何处理器处理就能直接在客户端上运行。这也是为什么希望在CSS中有原生变量,嵌套等特性。

真正的CSS嵌套

关于将嵌套纳入CSS规范的讨论已经持续了好几年(早在2005年 @Tab Atkins就提出了CSS的原生嵌套),同时在 Github上发起相关的讨论。在2019年3月初,CSS嵌套模块(CSS Nesting Module)的第一部分编辑草案发布了。该草案概述了一种未来的机制,通过这种机制,我们将能够对CSS选择器进行本地嵌套。例如,在纯CSS中,不使用任何处理器程序,也不需要经过任何编译器编译,就能被客户端识别,解析。

这个模块描述了对一个样式规则嵌套在另一个样式规则中的支持,允许内部规则的选择器引用外部规则所匹配的元素。这个功能允许相关的样式在CSS样式表中被聚合成一个单一的结构,提高了可读性和可维护性。

CSSWG已经同意发布该模夫的第份公共工作草案,使其成为建议的一部分。这样一来,事情变得正式了。也可以说,原生的CSS嵌套已经离我们越来越近了。

不过正如草案中所说:

它仅用于讨论,并可以在任何时候改变。在此公布并不意味着 W3C 对其内容认可。

简单地说,接下来讨论的东西,很有可能在未来或者任何时候都会改变,不建议直接用于生产中。但,不要让它影响我们的热情,因为它几乎与我们在处理器中所提到的嵌套功能相同。

接下来,我们通过和SCSS对比的方式,来看看CSS原生嵌套的相关规则和使用。

CSS 嵌套是什么样子的?

熟悉Sass的同学对嵌套应该不会感到陌生,它看起来像这样:

// SCSS
div {
    background: #fff;

    p {
        color: red;
    }
}

编译出来的CSS:

div {
    background: #fff;
}

div p {
    color: red;
}

可以使用 Sassmeister 在线查看编译后的代码!

用原生 CSS 的嵌套来编写,看上去和 Sass 的非常相似:

/* CSS */
div {
    background: #fff;

    & p {
        color: red;
    }
}

你会注意到在 p 选择器前面有一个 & 符号,这个符号在此称为 嵌套选择器,它将告诉浏览器,你想把p嵌套在div

其实在Sass处理器中也有这样的&符,比如:

// SCSS
div {
    background: #fff;

    & p {
        color: red;
    }
}

/*编译出来的 CSS*/
div {
    background: #fff;
}

div p {
    color: red;
}

但在Sass处理器使用 & 符,一般不像上面这样使用,因为有点多此一举,画蛇添足的感觉。在Sass中使用&符,一般只有当你想在前面的选择器中添加一些东西(比如一个类)时,才使用:

// SCSS
div {
    background: #fff;

    &.red {
        color: red;
    }
}

/* 编译出来的 CSS */
div {
    background: #fff;
}

div.red {
    color: red;
}

在 Sass 中,&除了可以放在一个选择器前面让其变成 .select1.select2之外,还可以让其放在一个选择器后面:

// SCSS
div {
    background: #fff;

    &.red {
        color: red;
    }
    
    .blue & {
        color: blue;
    } 
}

/* 编译出来的 CSS */
div {
    background: #fff;
}

div.red {
    color: red;
}

.blue div {
    color: blue;
}

& 放在一个选择器之后,将变编译成.select2 .select1这样。

但在原生的 CSS 嵌套中,这样使用是一种错误的语法规则,即,下面的代码不正确:

/* CSS */
div {
    background: #fff;

    .blue & {
        color: blue;
    }
}

虽然上面代码的嵌套规则语法不成立,但并不代表在原生CSS就没有类似的功能。在CSS原生嵌套中:

如果要成为嵌套前缀,一个嵌套选择器必须是选择器的第一个复合选择器中的第一个简单选择器。如果选择器一个选择器列表,列表中的每一个复合选择器都必须是嵌套的,才能使选择器作为一个整体嵌套。

简单地说,规范中提供了另一个特性,使用嵌套规则 @nest,就可以把 & 放在其他地方。通过给选择器加上 @nest 前缀,你可以选择你想让选择器嵌套的位置。

/* CSS */
div {
    background: #fff;

    @nest .blue & {
        color: blue;
    }
}

上面的代码和下面这段Sass代码达到同等的功能:

// SCSS
div {
    background: #fff;

    .blue & {
        color: blue;
    }
}

从上面的示例中,我们可以发现,CSS原生嵌套中 &@nest 对我们很重要。其中:

  • & 是一个选择器,称为 嵌套选择器
  • @nest 是一个规则(@规则),用来描述或定义选择器嵌套的规则

如果想彻底的了解和掌握CSS嵌套的使用,就必须对&@nest有深入的了解。

CSS嵌套选择器 &

当使用一个嵌套的样式规则时, 必须能够引用父规则所匹配的元素 ,这也是嵌套的全部意义所在。比如我们有一个这样的 HTML结构:

<!-- HTML -->
<div class="foo">
    <div class="faz"></div>
</div>
<div class="bar"></div>

下面两段代码,其中一段对于嵌套是无意义的:

/* 符合嵌套样式规则 */
.foo {
    background: #fff;

    & .faz {
        color: red;
    }
}

/* 不符合嵌套规则 */
.foo {
    background: #fff;

    & .bar {
        color: red;
    }
}

在 CSS 嵌套模块中,为了实现“样式嵌套必须能够引用父规则所匹配的元素”,规范中定义了一个新的选择器,即 嵌套选择器,也就是 & 符。

当在一个嵌套样式规则的选择器中使用时,嵌套选择器代表被父规则匹配的元素。当在任何其他情况下使用时,它不代表任何东西。也就是说,它是有效的,但没有匹配任何元素。

比如下面这个示例:

/* CSS */
.foo {
    color: red;

    & .bar {
        color: blue;
    }
}

示例中的 & 其实代表的就是父选择器 .foo

另外,CSS 的嵌套选择器可以通过用父级样式规则的选择来代替它,用一个 :is()选择器来包装。比如说:

/* CSS */
a, b {
    & c { color: blue; }
}

/* 等效于 */
:is(a, b) c {
    color: blue;
}

嵌套选择器的权重等于父级样式规则的选择器中匹配给定元素的最大权重。比如下:

/* CSS */
#a, .b {
    & c { 
        color: blue; 
    }
}

<!-- HTML -->
<div id=a>
    <c>foo</c>
</div>

上面的 CSS 代码相当于:

#a c, 
.b c { 
    color: blue; 
}

& 选择器的权重是:

  • & 匹配父选择器 #a 时,它的权重是 [1, 0, 0],整个选择器 #a c 的权重是 [1, 0, 1]
  • & 匹配父选择器 .b 时,它的权重是 [0, 1, 0],整个选择器 .b c 的权重是 [0, 1, 1]

嵌套选择器被允许在复合选择器的任何地方,甚至在类型选择器之前,违反了对复合选择器内排序的正常限制。

注意,这是允许直接嵌套的要求,另外,“类型选择器必须在前”,这背后没有任何内在的原因;它的存在是因为我们需要在复合选择器中直接将简单的选择器附加在一起时,能够明确地将它们区分开来,而且从.foodiv中也看不出它的含义与div.foo相同。不过,&是可以明确地从一个标识中分离出来的,所以它在一个类型选择器前面没有问题,比如 &div

嵌套样式规则

不幸的是,将样式规则天真地嵌套在其他样式规则中是有问题的。即,“选择器的语法与声明的语法是不明确的,所以需要一个较好的判断,来判断一个给定的文本是一个声明还是一个样式规则的开始”。到目前为止,CSS在解析时只需要一个超前的令牌(Token of lookahead)单符号的,这个缺点在CSS实现中通常被认为是不可接受的。

为了绕开这个限制,本规范定义了两种将样式规则嵌套到其他样式规则中的方法,这两种方法都被设计成与周围的声明明确一致。第一种是直接嵌套,它有一些限制性语法,但是以消除歧义语法的形式施加了最小的额外“权重”,适合大多数情况。第二种是 @nest规则,它施加了一个很小的语法权重,以使其与周围的声明区分开来,但对选择器构成没有限制。这两种规则在其他方面是等价的,样式表开发者可以根据需要使用其中一种。

直接嵌套

如果一个样式规则的选择器是嵌套的,它可以直接嵌套在另一个样式规则中。

要被嵌套,一个嵌套的选择器必须是该选择器的第一个复合选择器中的第一个简单选择器。如果选择器是一个选择器的列表,列表中的每一个复合选择器都必须是嵌套式的,才能使选择器作为一个整体被嵌套。

比如,下面的嵌套是有效的:

.foo {
    color: blue;

    & > .bar { 
        color: red; 
    }
}
/* 等同 */
.foo { 
    color: blue; 
}

.foo > .bar { 
    color: red; 
}


.foo {
    color: blue;

    &.bar { 
        color: red; 
    }
}
/* 等同 */
.foo { 
    color: blue; 
}

.foo.bar { 
    color: red; 
}


.foo, .bar {
    color: blue;

    & + .baz, &.qux { 
        color: red; 
    }
}
/* 等同 */
.foo, .bar { 
    color: blue; 
}

:is(.foo, .bar) + .baz,
:is(.foo, .bar).qux { 
    color: red; 
}

.foo {
    color: blue;

    & .bar & .baz & .qux { 
        color: red; 
    }
}
/* 等同 */
.foo { 
    color: blue; 
}

.foo .bar .foo .baz .foo .qux { 
    color: red; 
}


.foo {
    color: blue;

    & { 
        padding: 2ch; 
    }
}
/* 等同 */
.foo { 
    color: blue; 
}

.foo { 
    padding: 2ch; 
}

/* 也等同 */

.foo {
    color: blue;
    padding: 2ch;
}

.error, #404 {
    &:not(.foo,.bar) > .baz { 
        color: red; 
    }
}
/* 等同 */
:is(.error, #404):not(.foo,.bar) > .baz { 
    color: red; 
}

.foo {
    &:is(.bar, &.baz) { 
        color: red; 
    }
}
/* 等同 */
.foo:is(.bar, .foo.baz) { 
    color: red; 
}

figure {
    margin: 0;

    & > figcaption {
        background: hsl(0 0% 0% / 50%);

        & > p {
            font-size: .9rem;
        }
    }
}
/* 等同 */
figure { 
    margin: 0; 
}

figure > figcaption { 
    background: hsl(0 0% 0% / 50%); 
}

figure > figcaption > p { 
    font-size: .9rem; 
}

main {
    & > section,
    & > article {
        background: white;

        & > header {
            font-size: 1.25rem;
        }
    }
}
/* 等同 */
main > :is(section, article) { 
    background: white; 
}

main > :is(section, article) > header { 
    font-size: 1.25rem; 
}

你可能在编写 CSS 的时候会使用 BEM 规则。我们在 Sass 中使用 BEM 可以像下面这样的编写:

//SCSS
.card {
    color: red;
    
    &__title {
        color: blue;
        
        &--sm {
            font-size: 12px;
        }
    }
    
    &__body {
        padding: 12px;
    }
    
    &--primary {
        color: blue;
    }
}

/* 编译出来的 CSS */
.card {
    color: red;
}

.card__title {
    color: blue;
}

.card__title--sm {
    font-size: 12px;
}

.card__body {
    padding: 12px;
}

.card--primary {
    color: blue;
}

但在 CSS 原生嵌套中,如果类似像Sass这样使用 BEM的话,得到的结果和你预期的不一样。比如:

/* CSS */
.foo {
    color: blue;

    &__bar {
        color: red;
    }
}

/* 等同于 */
.foo {
    color: blue;
}

__bar.foo {
    color: red;
} 

注意,__bar 是一个有效的潜在的自定义元素

我们再来看几个无效的CSS嵌套的使用示例:

/* 无效:因为没有使用嵌套选择器 & */
.foo {
    color: red;

    .bar {
        color: blue
    }
}

/* 无效:因为嵌套选择器 & 不在第一个复合选择器中 */
.foo {
    color: red;

    .bar & {
        color: blue;
    }
}

/*无效:第二个& 不再是有效的嵌套的选择器 */
.foo {
    color: red;

    &&.bar {
        color: blue;
    }
}

/*无效:因为列表选择器中的第二个选择器前没有嵌套选择器 & */
.foo {
    color: red;

    &.bar, .baz {
        color: blue;
    }
}

/*无效: 只能在所有常规 CSS规则之后添加嵌套选择器,在嵌套选择器之后的任何CSS属性将被忽略 */
.foo {
    color: red;

    &.bar {
        color:blue;
    }

    background: #fff;
}

被嵌套:@nest

虽然直接嵌套看起来不错,但它有些脆弱。一些Sass中有效的嵌套选择器,比如 .foo &,在CSS原生嵌套中是不允许的,而且以某些方式编辑选择器会使规则意外的失效。简单地说,在原生CSS嵌套中,.foo & 是一个无效的规则。同样,有些人发现嵌套在视觉上很难与周围的声明区分开来。

为了帮助解决所有这些问题,规范中定义了@nest规则,它对如何有效地嵌套样式规则施加了较少的限制。

CSS的@nest语法规则很简单:

@nest = @nest <selector> { <declaration-list> }

@nest规则的功能与样式规则相同:它以一个选择器开始,并包含适用于选择器匹配元素的声明。唯一的区别是,@nest规则中使用的选择器必须是包含巢(nest-containing)的,这意味着它在某处包含了一个嵌套的选择器。如果一个选择器的列表中所有单独的复合选择器都是含巢(nest-containing)的,那么它就是含巢(nest-containing)的。

我们可以像下面这样在CSS中使用@nest

.foo {
    color: red;

    @nest & > .bar {
        color: blue
    }
}

/* 等同 */
.foo {
    color: red;
}

.foo > .bar {
    color: blue;
}

.foo {
    color: red;

    @nest .parent & {
        color: blue;
    }
}
/* 等同 */
.foo {
    color: red;
}

.parent .foo {
    color: blue;
}

.foo {
    color: red;

    @nest :not(&) {
        color: blue;
    }
}

/* 等同 */
.foo {
    color: red;
}

:not(.foo) {
    color: blue;
}

不过,像下面这样使用则是无效的:

/* 无效:没有嵌套选择器 & */
.foo {
    color: red;

    @nest .bar {
        color: blue;
    }
}

/* 无效:因为列表中不是所有的选择器都是包含一个嵌套选择器 */
.foo {
    color: red;

    @nest & .bar, .baz {
        color: blue;
    }
}

条件嵌套

在 CSS 中可以让 CSS 样式规则在符合一定条件下才能生效(即 CSS 中的条件CSS),比如我们熟悉的@media@supports@viewport,以及最新的 @container@ 规则。 在Sass中,我们可以像下面这样编写:

// SCSS
.card {
    color: red;
    
    @media (min-width: 40rem) {
        color: blue;
    }
    
    @container (min-width: 30rem) {
        font-size: 16px;
    }
    
    @supports (display: grid) {
        display: grid;
    }
}

编译出来的结果如下:

/* CSS */
.card {
    color: red;
}

@media (min-width: 40rem) {
    .card {
        color: blue;
    }
}

@container (min-width: 30rem) {
    .card {
        font-size: 16px;
    }
}

@supports (display: grid) {
    .card {
        display: grid;
    }
}

在原生CSS的嵌套中,也有条件嵌套规则。我们可以像下面这样编写:

.foo {
    display: grid;

    @media (orientation: landscape) {
        & {
            grid-auto-flow: column;
        }
    }
}
/* 等同 */
.foo {
    display: grid
}
@media (orientation: landscape) {
    .foo {
        grid-auto-flow: column;
    }
}

.foo {
    display: grid;

    @media (orientation: landscape) {
        & {
            grid-auto-flow: column;
        }

        @media (min-inline-size > 1024px) {
            & {
                max-inline-size: 1024px;
            }
        }
    }
}
/* 等同于 */
.foo {
    display: grid
}

@media (orientation: landscape) {
    .foo {
        grid-auto-flow: column;
    }
}

@media (orientation: landscape) and (min-inline-size > 1024px) {
    .foo {
        max-width: 1024px;
    }
}

.foo {
    color: red;

    @media (min-width: 480px) {
        & > .bar,
        & > .baz {
            color: blue;
        }
    }
}
/* 等同于 */
.foo {
    color: red
}

@media (min-width: 480px) {
    .foo > .bar,
    .foo > .baz {
        color: blue;
    }
}

@supports@container也可以像@media那样使用,来看一个@supports的示例:

.foo {
    color: red;

    @supports (display: grid) {
        & {
            display: grid;
            
            @supports (rotate: 30deg) {
                & {
                    rotate: 30deg;
                }
            }
        }
    }
}
/* 等同于 */
.foo {
    color: red
}

@supports (display: grid) {
    .foo {
        display: grid
    }
}

@supports (display: grid) and (rotate: 30deg) {
    .foo {
        rotate: 30deg;
    }
}

不过下面这样的使用姿势将是无效的:

/* 无效:没有嵌套选择器 & */
.foo {
    display: grid;

    @media (orientation: landscape) {
        grid-auto-flow: column;
    }
}

/* 无效:因为列表中不是所有的选择器都是包含一个嵌套选择器 */
.foo {
    color: red;

    @media (min-width: 480px) {
        & h1, h2 { 
            color: blue; 
        }
    }
}

/* 无效:因为@nest期望有一个选择器前奏。而是提供了一个条件性规则 */
.foo {
    color: red;

    @nest @media (min-width: 480px) {
        & { 
            color: blue; 
        }
    }
}

嵌套规则和样式规则混合

在 Sass 中,嵌套可以发生在任意位置,比如:

// SCSS
.foo {
    color: red;

    &.bar {
        color: blue;
    }
    
    background: #fff;
    
    .baz & {
        background: #09f;
    }
    
    border: 2px solid red;
}

编译出来的CSS代码:

.foo {
    color: red;
    background: #fff;
    border: 2px solid red;
}

.foo.bar {
    color: blue;
}

.baz .foo {
    background: #09f;
}

在原生CSS的嵌套中也可以像Sass中一样编写嵌套规则(样式规则和嵌套规则混合使用):

article {
    color: green;

    & { 
        color: blue; 
    }

    color: red;
}

但原生CSS这样写和Sass还是有很大差异的。比如上面的代码,color:red规则是无效的,将会被忽略。因为 它发生在嵌套的样式规则之后

然而,紧接着的嵌套规则仍然有效,比如:

article {
    color: green;

    & { 
        color: blue; 
    }

    color: red; /* 无效: 会被忽略*/

    /* 有效 */
    &.foo { 
        color: yellow; 
    }
}

上面的代码等同于:

article {
    color: green;
}

article {
    color: blue;
}

article.foo {
    color: yellow
}

嵌套的样式规则和嵌套的条件规则的相对顺序是很重要的;一个给定的样式规则中和其中的嵌套样式规则有可能匹配同一个元素,如果这两个规则的权重是相等的,那么适用的声明在样式表中的相对顺序决定了哪个声明在级联中“获胜”。为此,一个嵌套的规则被认为是在其父规则之后。

例如,在权重相同的情况下,使用级联来解决哪个样式规则被使用:

article {
    & .blue { /* (0, 1, 1)*/
        color: blue; 
    } 

    & .red  { /* (0, 1, 1)*/
        color: red; 
    }  
}

/* 等同 */
article .blue {
    color: blue
}

article .red {
    color: red
}

如果上面的样式代码用于下面这样的 HTML:

<article>
    <div class="red blue"></div>
</article>

那么color:red被取胜。虽然 article .bluearticle .red 的选择器权重都是 (0, 1, 1),但article .redarticle .blue后面,所以后者取胜。

再看下面这个示例:

article {
    color: blue;

    & { 
        color: red; 
    }
}

上面的代码等同:

article {
    color: blue;
}

article {
    color: red;
}

这两个样式声明都有相同的权重(0, 0, 1),但嵌套规则被认为是在其父规则之后,所以color: red获取。

再来看带有 @nest 规则的嵌套:

article {
    color: blue;

    @nest :where(&) { 
        color: red; 
    }
}

上面代码等同于:

article {
    color: blue;
}

:where(article) {
    color: red;
}

:where()伪类将嵌套选择器的权重降低到0,所以 color: red 权重是(0, 0, 0),比color: blue的权重(0,0,1)要低,因此color:blue将运用于article选择器上。

现在可以使用CSS嵌套?

到目前为止,还没有浏览器支持原生的 CSS 嵌套。如果在 CSS 中直接使用原生的嵌套,浏览器会将嵌套的规则忽略,如下图所示:

但如果你现在就希望在项目中使用原生 CSS 嵌套相关的特性,可以使用 PostCSSpostcss-preset-env,他会将你的嵌套 CSS 转换为目前所有浏览器能理解的 CSS。

PostCSS的使用很简单,现在在工程构建的配置(比如 WebpackParcelGulpViteSnowpack等)中都会配置 PostCSS的能力,相关的配置可以在PostCSS官方文档找到,你也可以根据自己的需要选择最为适合的方式。

如果你只是想试玩一下,那可以在 postcss-preset-env 的官网上“Try It Now”试玩

改变 "Stage" 的配置会有不同的输出结果:

你可能已经发现了,这里是内嵌了Codepen的页面,也就是说,你也可以直接在 Codepen 上使用原生CSS的嵌套,但在 CSS 配置项中,需要选择PostCSS,并且配置相关的PostCSS插件,比如:

@use postcss-preset-env{
    stage: 0;
    browsers: last 2 versions
}

.box {
    color: red;
    
    & .foo {
        color: blue;
    }
}

如果你希望在原生CSS使用嵌套和 Sass中的嵌套习惯的话,还可以使用 PostCSS 的另一个插件 postcss-nested

另外,在自己的项目中还可以通过安装 PostCSS CLI 和 postcss-preset-env ,在命令终端上使用。

## 安装PostCSS CLI
[$] <> npm i postcss-cli -g

## 在项目中安装PostCSS 和 postcss-preset-env
[$] <> npm i postcss postcss-preset-env --save-dev

这个时候你的项目的根目录下会自动创建一个 package.json 文件,并且会添加安装的相关依赖:

//package.json
{
    "devDependencies": {
        "postcss": "^8.3.6",
        "postcss-preset-env": "^6.7.0"
    }
}

安装好相关的依赖(比如PostCSS CLI,postcss 和 postcss-preset-env等),需要在项目根目录底下创建一个postcss.config.js 文件,并在该文件中添加PostCSS及其插件相关的配置:

// postcss.config.js
const postcssPresetEnv = require('postcss-preset-env');

module.exports = {
    plugins: [
        postcssPresetEnv({
            stage: 0,
            browsers: 'last 2 versions'
        })
    ]
}

在项目中创建一个名为input.css样式文件(可以是任意你喜欢的文件名称),在该文件中编写你的CSS:

/* input.css */
.foo {
    color: red;

    & .bar {
        color: blue;
    }

    @nest .baz & {
        color: lime;
    }

    @media (min-width: 40em) {
        & {
        color: yellow;
        }
    }
}

保存input.css文件之后,在命令终端执行类似下面的这样的命令:

postcss path/to/input.css -o path/to/output.css
  • postcss 指的是 postcss 命令
  • path/to 指的是输入文件和输出文件的路径,输入和输出的文件路径可以不同,需要具体根据你自己项目中文件位置来配置
  • input.css 输入的样式文件名,文件名可以是任意你喜欢的文件名
  • output.css 输出的样式文件名,文件名可以是任意你喜欢的文件名

比如,我们这个示例,命令终端执行下面的命令:

[$] <> postcss ./input.css -o ./output.css

这个时候,项目中会自动创建一个output.css,并且结过PostCSS编译后的代码放置这个文件中:

/* output.css */
.foo {
    color: red
}

.foo .bar {
    color: blue;
}

.baz .foo {
    color: lime
}

@media (min-width: 40em) {
    .foo {
        color: yellow;
    }
}

注意,你可以按同样的方式,在postcss.config.js配置其他的PostCSS插件!

如果你想了解在 Webpack 中配置PostCSS,除了阅读官网文档之外,还可以阅读:

不良的 CSS 嵌套带来的破坏性

前面我们花了较大的篇幅介绍了原生CSS嵌套的一些规则和基本使用的示例,并且告诉你怎么在自己的项目中使用。虽然说 CSS 嵌套给我们编码带来一些便利性,但不良的嵌套规则将会给我们带来一些破坏性,比如 选择器权重

规范在介绍嵌套选择器 & 时提供了一个像下图这样的示例:

CSS的嵌套选择器被解析的时候,浏览器会用:is()来包裹父选择器,正如上图所示,& 的父选择器ab被伪类选择器:is()包裹,即:is(a, b),这将会影响选择器权重。

那我们就得先从:is()选择器开始说起。

CSS的 :is() 选择器是较新的一个选择器(CSS Selectors Level 4)。在:is()选择器中可以包裹一堆不同的选择器,以防重复。比如:

main :is(h1, h2, h3) {
    font-family: inherit;
}

/* 等同 */

main h1,
main h2,
main h3 {
    font-family: inherit;
}

从上面示例代码可以看出使用:is()选择器的优势,只需要写一次main就可以(即:is(main))。当涉及到选择器权重时,:is()选择器从列表中最大权重的项目中获得其选择器权重。

稍微了解CSS的同学都知道,CSS选择器权重在CSS领域是非常基础且又非常重要的一个概念:

如果你对CSS选择器权重不够了解的话,可以通过下图来理解:

也可以使用一些在线的CSS选择器权重计算器来帮助你理解。

我个人比较喜欢的两个在线CSS选择器权重的计算器分别是 @Kilian Valkhof(上图所示)@Keegan Street 设计的。正如上图所示,你只需要输入你想要计算的CSS选择器,它就会根据选择器权重计算规则自动帮你计算出来。

.box.foo > li > a[href="https://"]::before » (0,3,3)

我们回到:is()选择器。比如上面示例中的:is(h1, h2, h3)。在在线选择器权重计算器中输入:is(h1, h2, h3),计算出来的权重是 (0,0,1)

这是一个权重很低的选择器,因为里面只有元素。如果在:is()里面的h1,h2,h3换成:is(#title, h2.title, h3),即:is(#title, h2.title, h3),此时选择器权重变成了(1,0,0)

这是因为:is()中包裹的三个选择器#titleh2.titleh3,权重分别是:(1,0,0)(0, 1, 1)(0, 0, 1)

其中#title的选择器权重最大,因此:is(#title, h2.title, h3) 的权重取三个选择器中最大的,即#title的。

如果每次都打出整个选择器,能很容易的知道选择器权重。而嵌套可以让你省去一些选择器的编写,但不幸的是,你可能对于选择器权重不好把握(不好一眼看出选择器权重是多少)。比如,你一不小心写了像下面这样的嵌套代码:

main {
    color: red;

    & #info {
        padding: 10px;

        & .newsitems {
            border-bottom: 1px solid currentColor;

            & div.newsitem {
                font-size: 14px;

                & h2 {
                    font-weight: 600;
                }

                & p {
                    font-size: 12px;

                    &.meta {
                        color: #333;


                        & span.date {
                            margin: 0 5px;
                        }

                        & + p {
                            margin-top: 2rem;
                        }
                    }
                }

                & a {
                    text-decoration: none;
                }
            } 
        }
    }
}

上面的代码等同于:

main {
    color: red
}

main #info {
    padding: 10px
}

main #info .newsitems {
    border-bottom: 1px solid currentColor 
}

main #info .newsitems div.newsitem {
    font-size: 14px
}

main #info .newsitems div.newsitem h2 {
    font-weight: 600;
}

main #info .newsitems div.newsitem p {
    font-size: 12px
}

main #info .newsitems div.newsitem p.meta {
    color: #333
}

main #info .newsitems div.newsitem p.meta span.date {
    margin: 0 5px;
}

main #info .newsitems div.newsitem p.meta + p {
    margin-top: 2rem;
}

main #info .newsitems div.newsitem a {
    text-decoration: none;
}

特别声明,上面这样写代码是一个不好的习惯,选择器嵌套过深,样式计算会更复杂,需要耗时更多,会影响页面渲染!实际开发中,切记不要像上面这样编写CSS,即使这些代码都是隶属于一个组件中。

你可能已经感觉到了,像上面的代码,有很多选择器,你并不知道他的权重是多少,比如 main #info .newsitems div.newsitem p.meta + p。在实际开发中,你总不可能使用选择器权重计算器,一个一个去把权重计算出来吧:

main                                                 » (0, 0, 1)
main #info                                           » (1, 0, 1)
main #info .newsitems                                » (1, 1, 1)
main #info .newsitems div.newsitem                  » (1, 2, 2)
main #info .newsitems div.newsitem h2               » (1, 2, 3)
main #info .newsitems div.newsitem p                » (1, 2, 3)
main #info .newsitems div.newsitem p.meta           » (1, 3, 3)
main #info .newsitems div.newsitem p.meta span.date » (1, 4, 4)
main #info .newsitems div.newsitem p.meta + p       » (1, 3, 4)
main #info .newsitems div.newsitem a                » (1, 2, 3)

你可能已经意识到了,CSS嵌套如果写的不足够好,你会发现自己给自己创造了很多很复杂的选择器,即构建了权重很大的选择器。给自己带来意想不到的麻烦,甚至给自己排查问题带来不确定因素。如果是这样的话,我想你也失去了使用CSS嵌套的初衷和意义。

嵌套的确具有强大的诱惑力,因为他能让 Web 开发者在编码的时候码很多代码(少写选择器),但事实上呢?很多 Web 开发者在使用嵌套的时候,时常出现上面示例代码那种现象,即 深层次的嵌套。但为了防止这种深层次嵌套引起选择器权重的问题(甚至还有可能影响 Web 页面的渲染性能),在编码的时候要限制这种深层次嵌套,并在可能的时候打破嵌套。

在编码过程,我们是可以通过一些技术手段来防止这种现象出现的。比如说,我们在自己的工程中使用像 StyleLint 的 CSS 检测工具。它的使用非常简单,只需要设置一个最大嵌套深度(max-nesting-depth)的规则即可。StyleLint相关的配置一般都放在项目根目录下的.stylelintrc文件中:

// .stylelintrc
{
    "rules": {
        "max-nesting-depth": 3
    }
}

一般将嵌套层级的限制(max-nesting-depth)设置为 3 是一个较好的效果。也就是说,即使我们不使用 StyleLint 这样的工具,我们在编码的时候层级的嵌套也不要超过三级。

我们可以把嵌套的 CSS 看成是一套独立的样式,比如按一个组件或嵌套的上下文来拆分CSS嵌套的层级。比如上面的代码嵌套代码,我们可以拆分成:

main {
    color: red;
}

#info {
    padding: 10px;

    & .newsitems {
        border-bottom: 1px solid currentColor;
    }
}

.newsitem {
    font-size: 14px;

    & h2 {
        font-weight: 600;
    }

    & p {
        font-size: 12px;
    }

    & .meta {
        color: #333;

        & span.date {
            margin: 0 5px;
        }

        & + p {
                margin-top: 2rem;
        }
    }

    & a {
        text-decoration: none;
    }
} 

上面的代码等同于:

main {
    color: red;
}

#info {
    padding: 10px
}

#info .newsitems {
    border-bottom: 1px solid currentColor;
}

.newsitem {
    font-size: 14px
}

.newsitem h2 {
    font-weight: 600;
}

.newsitem p {
    font-size: 12px;
}

.newsitem .meta {
    color: #333
}

.newsitem .meta span.date {
    margin: 0 5px;
}

.newsitem .meta + p {
    margin-top: 2rem;
}

.newsitem a {
    text-decoration: none;
}

打破深层嵌套就一个基本原则:按上下文(或组件级别)对选择器进行嵌套,而且嵌套层级不超过三层! 除了这个基本原则,我们在编码的时候还可以按下面的一些规则来编写 CSS 的嵌套:

  • 布局样式和组件样式之间分割:把布局样式放在嵌套选择器中,把样式逻辑放在顶层选择器中
  • 独立的组件样式:把一个组件内的所有样式放在一个选择器容器内
  • 保持空的父元素:当一个元素只在一个父元素中使用时,可以保持嵌套的顶层为空,只用它来定义选择器

比如:

/* 布局和组件样式分割 */
.layout {
    display:flex;
    justify-content: flex-start;
    align-items: flex-start;
    gap: 1rem;
    
    & > .component {
        flex: 1 1 33%;
    }
}

.component {
    background: #fff;
    color: #332;
}

/* 父容器为空,只当作选择器 */
.foo {
    /* 无样式 */
    & > div {
        flex: 1 1 33%;
        background: #fff;
    }
}

.bar {
    /* 无样式 */
    & h2 {
        color: #333;
    }
}

/* 独立组件 */
.component {
    background: #fff;
    
    &.col-1-3 {
        flex: 1 1 33%;
    }
    
    &.full {
        flex: 1 0 100%;
    }
    
    & h2 {
        color: #333;
    }
}

这些规则也不是绝对的,很多时候还是取决于你的编码习惯。但需要注意的是,在使用 CSS 嵌套时,要有意识地付出代价。如果你让他过于自由,那将会让你的 CSS 代码很难维护;但如果有意使用(坚持一定原则和规则),它将是一个组织你CSS,和减少重复代码的好方法。

小结

在文章中,主要和大家探讨了原生 CSS 嵌套的语法规则和基本使用。从文章中的示例中不难发现,CSS原生嵌套的使用和 CSS处理器中的嵌套的特性几乎相似,甚至使用规则都相似。不同的是,CSS 原生嵌套需要依赖嵌套选择器 &@nest,实现直接嵌套和被嵌套。

虽然 CSS 的嵌套给我们带来很多便利性,让你的代码也更易于阅读和理解,并且自由度也非常的高,但万物都是相生相克的,如果过于自由随意,也会给我们带来意想不到的缺陷,比如说嵌套过深,无意增加了选择器权重,也无意中让代码变得难于维护。事实上,不管是 CSS 处理器还是在原生CSS嵌套中使用嵌套,我们都应该遵守一个基本原则,嵌套层级不超过三层。只有这样,才能享受到 CSS 嵌套带来的红利,又不会增加代码的维护难度。

最后要说的,文章中所有内容在将来都有可能会有变化,但这里提到的很多特性也是 CSS 嵌套的基本特性。即使以后会变更,也不会有很大的变化。我们可以从现在开始了解学习他,并且一起推进他尽早得到浏览器支持。