CSS特异性搞得头大,BEM配:where能摆平?

2,131字
9–14 分钟
in

写样式的时候最怕啥?好不容易调好一个组件的颜色,结果页面其他地方莫名其妙跟着变了。BEM这套命名规矩就是为了解决这种翻车现场,但碰上:is():where()这些现代伪类,特异性又容易乱套。别慌,把BEM和:where()凑一块儿,能完美守住特异性的底线,顺便少写一堆冗余代码。

目录

BEM的特异性

BEM把组件拆成块(Block)、元素(Element)、修饰符(Modifier),比如.card.card__title.card--featured。每个类选择器在CSS层叠里的特异性分数都是0,1,0。这就好比排队时每人拿同一个号的票,谁后到谁生效,不会出现插队加塞的情况。大型项目(比如银行网站、大学官网)里成千上万个样式,全靠这个规矩才能不互相踩踏。

选择器特异性分数
.card0,1,0
.card__title0,1,0
.card--featured0,1,0

伪类的特异性坑

:not():is()这些伪类看着好用,但一不留神就会拉高特异性。举个例子:

/* 这货的特异性是 0,2,0 */
.something:not(.something--special) {
  color: red;
}
.something--special {
  color: blue;
}

明明.something--special写在后面,结果颜色还是红的。为啥?因为:not()本身不增加特异性,但它里面的参数.something--special是个类,导致整个选择器的特异性变成了0,2,0。这就好比原本大家都是普通票,突然有个人掏出了VIP票,后面的普通票自然干不过。

:where一键清零

:where()这个伪类牛就牛在它的特异性永远是0,0,0。哪怕写成:where(button#widget.some-class),分数还是零。用它把有坑的部分包起来,就能把特异性压回去:

/* 特异性完美回到 0,1,0 */
.something:where(:not(.something--special)) {
  color: red;
}
.something--special {
  color: blue;
}

现在.something--special排在后面,优先级正常,蓝颜色就能生效了。这操作相当于给VIP票盖了个“作废”章,大家又回到同一起跑线。

:where救场嵌套

很多人在Sass或Less里写嵌套时,为了给修饰符下的子元素单独写样式,会搞出下面这种写法:

.card { ... }
.card--featured { 
  .card__title { ... }
  .card__img { ... }
}
.card__title { ... }
.card__img { ... }

编译后.card--featured .card__title的特异性变成了0,2,0,比普通.card__title0,1,0高。以后想覆盖就费劲了。硬核解法是给每个子元素都加修饰符类:

<div class="card card--featured">
  <h3 class="card__title card__title--featured">...</h3>
  <img class="card__img card__img--featured" src="...">
</div>

这样写确实稳,但模板里要写一堆if判断,代码又臭又长。用:where()能偷个懒:

.card { ... }
.card--featured { ... }
.card__title { 
  :where(.card--featured) & { ... }
}
.card__img { 
  :where(.card--featured) & { ... }
}

:where(.card--featured)这部分特异性为零,所以.card--featured .card__title的总特异性还是0,1,0,跟普通的.card__title完全一样。谁后出现谁说了算,不用再给每个子元素单独加修饰符类了。真香。

搞定第三方样式

有时候不得不给第三方脚本插入的DOM写样式,那些元素可能带的是ID而不是类。#widget的特异性是1,0,0,比BEM的类高出一大截。硬写ID选择器会破坏整个项目的特异性平衡。可以用:where()把ID包起来:

/* 特异性降到 0,1,0 */
.page-wrapper :where(#widget) {
  background: #f0f0f0;
}

但这样依赖.page-wrapper这个父类存在,万一哪天父类名改了或者没这个父元素,就尴尬了。更皮实一点的办法是结合:is()

/* 照样是 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  background: #f0f0f0;
}

:is(.dummy-class, body)会取参数里最高的特异性——.dummy-class0,1,0body0,0,1,所以整体拿到0,1,0。再加上:where(#widget)的零分,最终选择器的特异性还是0,1,0.dummy-class这个类压根不用在HTML里存在,纯粹是个工具人,用来抬一手特异性分数。body标签永远在那儿,保证选择器能命中目标。这招虽然有点骚,但在处理脏数据的时候特别管用。