使用 BEM 和现代 CSS 选择器来驯服层叠样式表

Avatar of Liam Johnston
Liam Johnston

DigitalOcean 为您的旅程各个阶段提供云产品。立即开始使用 200 美元的免费额度!

BEM。就像前端开发世界中的所有技术一样,使用 BEM 格式编写 CSS 可能会两极分化。但至少在我的 Twitter 泡泡中,它是最受欢迎的 CSS 方法之一。

就我个人而言,我认为 BEM 很好,我认为您应该使用它。但我也理解为什么您可能不会使用它。

无论您对 BEM 的看法如何,它都提供了许多好处,最大的好处是它有助于避免 CSS 层叠中的特异性冲突。这是因为,如果使用得当,以 BEM 格式编写的任何选择器都应该具有相同特异性分数(0,1,0)。多年来,我为许多大型网站设计了 CSS(例如政府、大学和银行),而正是这些更大的项目让我发现 BEM 真的很棒。当您有信心您正在编写或编辑的样式不会影响网站的其他部分时,编写 CSS 会更有趣。

实际上,有一些例外情况,被认为完全可以添加特异性。例如::hover:focus 伪类。它们的特异性分数为 0,2,0。另一个是伪元素 - 例如 ::before::after - 它们的特异性分数为 0,1,1。不过,在本文的其余部分,让我们假设我们不希望出现任何其他特异性蠕变。🤓

但我并不是来向您推销 BEM 的。相反,我想谈谈我们如何将它与现代 CSS 选择器一起使用 - 例如 :is():has():where() 等 - 来获得对 层叠样式表 的更多控制。

现代 CSS 选择器是怎么回事?

CSS 选择器级别 4 规范 为我们提供了一些强大的新型选择元素的方法。我最喜欢的一些包括 :is():where():not(),它们都得到所有现代浏览器的支持,并且现在可以在几乎所有项目上安全使用。

:is():where() 基本上是一样的,区别在于它们对特异性的影响。具体来说,:where() 的特异性分数始终为 0,0,0。是的,即使 :where(button#widget.some-class) 也没有特异性。同时,:is() 的特异性是其参数列表中特异性最高的元素。因此,我们已经找到了两个现代选择器之间的层叠冲突区别,我们可以利用它们。

令人难以置信的强大 :has() 关系伪类也 正在迅速获得浏览器支持(在我看来,这是自 Grid 以来 CSS 最大的新功能)。但是,在撰写本文时,:has() 的浏览器支持还不够好,无法在生产环境中使用。

让我将其中一个伪类添加到我的 BEM 中……

/* ❌ specificity score: 0,2,0 */
.something:not(.something--special) {
  /* styles for all somethings, except for the special somethings */
}

糟糕!看到那个特异性分数了吗?请记住,使用 BEM,我们理想情况下希望我们的选择器都具有 0,1,0 的特异性分数。为什么 0,2,0 不好?考虑一个类似的扩展示例

.something:not(a) {
  color: red;
}
.something--special {
  color: blue;
}

即使第二个选择器在源代码顺序中排在最后,第一个选择器更高的特异性(0,1,1)会获胜,并且 .something--special 元素的颜色将设置为 red。也就是说,假设您的 BEM 编写正确,并且选定元素在 HTML 中同时应用了 .something 基类和 .something--special 修饰符类。

如果不小心使用,这些伪类可能会以意想不到的方式影响层叠样式表。而正是这些类型的差异会在以后造成麻烦,尤其是在更大更复杂的代码库中。

糟糕。现在该怎么办?

还记得我之前说过 :where() 和它的特异性为零吗?我们可以利用这一点

/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
  /* etc. */
}

这个选择器的第一部分(.something)获得了通常的特异性分数 0,1,0。但 :where() - 以及它内部的所有内容 - 的特异性为 0,这不会进一步增加选择器的特异性。

:where() 允许我们嵌套

那些不像我一样那么在意特异性的人(公平地说,可能很多人都是这样)在嵌套方面已经过得很好了。通过一些随意的键盘敲击,我们可能会得到这样的 CSS(注意,为了简洁起见,我使用的是 Sass)

.card { ... }

.card--featured {
  /* etc. */  
  .card__title { ... }
  .card__title { ... }
}

.card__title { ... }
.card__img { ... }

在这个例子中,我们有一个 .card 组件。当它是一个“精选”卡片(使用 .card--featured 类)时,卡片的标题和图片需要以不同的方式进行样式设置。但是,正如我们现在所知,上面的代码导致了与我们系统中其他部分不一致的特异性分数。

一个坚定的特异性狂热者可能会这样做

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

还不错,对吧?坦率地说,这是很漂亮的 CSS。

不过,HTML 中确实有一个缺点。经验丰富的 BEM 作者可能非常清楚,需要使用笨拙的模板逻辑才能将修饰符类有条件地应用于多个元素。在这个例子中,HTML 模板需要有条件地将 --featured 修饰符类添加到三个元素(.card.card__title.card__img)中,尽管在实际情况下可能更多。这需要很多 if 语句。

:where() 选择器可以帮助我们编写更少的模板逻辑 - 以及更少的 BEM 类 - 而不增加特异性级别。

.card { ... }
.card--featured { ... }

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

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

以下是相同的代码,但在 Sass 中(注意结尾的 和号

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

无论您是否应该选择这种方法而不是将修饰符类应用于各个子元素,这都是个人喜好问题。但至少 :where() 现在给了我们选择!

非 BEM HTML 怎么办?

我们生活在一个不完美的世界里。有时您需要处理超出您控制范围的 HTML。例如,第三方脚本注入的 HTML,您需要对其进行样式设置。这种标记通常不是使用 BEM 类名编写的。在某些情况下,这些样式根本不使用类,而是使用 ID!

:where() 再次为我们提供支持。这个解决方案略微有点 hacky,因为我们需要引用 DOM 树中更上层的某个元素的类,我们知道这个元素存在。

/* ❌ specificity score: 1,0,0 */
#widget {
  /* etc. */
}

/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
  /* etc. */
}

引用父元素感觉有点冒险和限制。如果该父类发生更改或由于某种原因不存在怎么办?更好的(但也可能是同样 hacky)解决方案是使用 :is()。请记住,:is() 的特异性等于其选择器列表中特异性最高的选择器。

因此,与其像上面的例子中那样使用 :where() 引用我们知道的(或希望!)存在的类,我们可以引用一个虚构的类和 <body> 标签。

/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  /* etc. */
}

永远存在的 body 将帮助我们选择 #widget 元素,并且 .dummy-class 类存在于同一个 :is() 中,这使 body 选择器具有与类(0,1,0)相同的特异性分数……并且使用 :where() 确保选择器的特异性不会高于此。

就是这样!

这就是我们如何利用 :is():where() 伪类的现代特异性管理功能以及我们在使用 BEM 格式编写 CSS 时获得的特异性冲突预防功能。在不久的将来,一旦 :has() 获得 Firefox 支持(在撰写本文时,它目前在旗帜后面获得支持),我们可能希望将它与 :where() 配合使用以撤消其特异性。

无论您是否完全使用 BEM 命名,我希望我们都能同意,选择器特异性的一致性是一件好事!