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 命名,我希望我们都能同意,选择器特异性的一致性是一件好事!
BEM,真是久违了。我还以为现在所有的年轻人都在 React 组件中使用 Tailwind 这样的实用框架呢。很高兴看到 BEM 重回视野。
谢谢,Bill。BEM 一直都在!:D
只是聚光灯转移了而已,就像我们行业中经常发生的那样。
Tailwind 等等对于某些类型的项目来说很好。但对于我经常遇到的那些大型 CMS 网站来说,我不会使用任何其他方法,只会使用 BEM(不过很少使用实用程序类)。
对于最后一个示例,我知道你想展示 `:is` 的强大功能,但为了保险起见,你也可以强调一下你可以使用 `[id]:where(#widget)` 甚至 `[id="widget"]`,它们都具有 `0,1,0` 的特异性。
哦,`[id="widget"]` 真是个好点子,我完全忘记了!
我有点糊涂了。为什么 `something:not(.something--special)` 会应用于具有 `something--special` 类的元素?这难道不是 `:not()` 运算符的本意吗?
哦,糟糕,我弄错了!这就是原因!我们会更新帖子中的内容。谢谢你指出错误 :)
一直在使用 `:has` 来做一些非常重要的事情……我真的很喜欢使用它们。
如果我们有 `:where()` 来防止特异性攀升,并且我们有选择器嵌套(无论是通过 SCSS 还是即将推出的原生嵌套),我们还需要 BEM 吗?也许我们现在可以将每个 CSS 块视为作用域内的,并使用更简单的类名。
很棒的问题。
除了帮助处理特异性之外,BEM 还提供了一些其他优势。
例如,我喜欢 BEM 的另一个原因是它可以明确地表明你在直接设置样式的元素。例如,假设有一个“卡片”组件的“标题”。你可以这样做
但是,如果你决定它们实际上应该是 `h2` 或 `h4`,会发生什么?你需要确保 HTML 也更新了。而且,如果你从 HTML 中删除了这样的内容,你就无法轻易知道要删除哪些(如果有的话)CSS。
(我意识到你并不需要使用 BEM 来考虑向你的 `h3` 元素添加类 - 但关键是,使用 BEM 意味着你想要设置样式的任何东西都应该添加类。)
你问题的第二部分是关于更简单的类名。我理解没有人喜欢一直键入 `.really__loooooong–classnames`。但这让我想到我喜欢 BEM 的另一个原因 - **唯一性**。为每个类添加完整的上下文意味着我可以更容易地搜索并找到它们的用法。例如,我可以搜索 `card__title` 并知道我不会得到来自其他组件的任何“标题”。这就是为什么我要避免这种 Sass 嵌套风格
好处是它不会增加特异性,但坏处是你不能可靠地找到代码用法。
尽管如此,你并不需要专门使用 BEM 来实现这些相同的原则。最终,这将取决于你(以及你的团队)的首选工作方式。如果你想使用一个类似于 `card-title` 的“元素”等效类,那就尽情使用吧。这篇文章的重点是关于管理特异性。BEM(仍然)被广泛使用,所以我重点介绍了它 :)
我认为这是一篇非常棒的文章。我知道这不是这篇文章的主要目的,但我认为对 BEM 优势的描述比我读过的其他大多数文章都更清楚。
就我个人而言,我更喜欢 SUIT CSS 命名 - 我们使用的是命名约定,而不是其他 SUIT 工具。我更喜欢 SUIT,因为它的命名选择更适合我们的其他代码(TypeScript 和 C#)。我们不是纯粹主义者,我们会使用组合(子级或兄弟级)选择器,以使名称更易于阅读和维护 HTML - 当然,这也影响了特异性。
有了 `:where` 和 `:is` 以及 层叠层,它们在常青浏览器中都具有良好的支持,我们现在有了命名约定的工具,可以优先考虑干净的 HTML 和 CSS 设计,而不必过多地担心特异性。我认为可能会有下一代 BEM 或 SUIT(或任何其他命名约定)来提高清晰度,方法是取消之前设计中对特异性要求的限制。
如果你使用 phpstorm(或者我假设的其他 jetbrains ide),那么 `&__title` 格式就可以使用。它允许你通过命令/ctrl 点击类名,程序就可以把你链接到正确的 CSS。
我不确定其他程序,但可能会有可用的插件来帮助你完成这项工作。
@Dan Christofi
很有意思。phpstorm/jetbrains 能够做到这一点很好。不过,我猜你仍然不能对整个代码库搜索“card__title”吧?
我刚刚在我的编辑器(VS Code)中尝试了你的建议,它不起作用。话虽如此,即使没有 Sass 嵌套,它也不起作用。我想我需要启用某个插件/设置。
即使如此,对我个人来说,缺乏可靠的代码库文本搜索是一个决定性因素。
我认为 `special:not(a)` 的特异性评分是 `0,1,1`,而不是 `0,2,0`。我实际上被搞糊涂了,去查看了 `:not` 的文档。
哎呀!在指出了另一个错误之后,我已经更新了那个被诅咒的代码块,但没有更新下面的解释。所有问题都已修复!
感谢这篇文章,BEM + SASS 对我来说仍然有效!
如果你需要一个 `:where()` 的根选择器,就像最后一个示例
/* ✅ 特异性评分:0,1,0 */
:is(.dummy-class, body) :where(#widget) {
/* 等等。 */
}
我认为你可以直接使用 `:root`
:root :where(#widget) {
/* 等等。 */
}
Jonathan,很好的建议。
我直到上周(上周)才知道 `:root` 选择器的特异性是 0,1,0。这绝对比我那些笨拙的想法更优雅 :)
我认为这里提到的 `:is(.dummy-class, body)` 可能是我最近在 CSS-tricks 上读到的最妙的东西了,我喜欢你表达的方式,以及你建议将它用于这个目的的方式。
✔️ 完全是 hack
谢谢,Tamm!我一直对这个想法感到很满意,直到有人指出你可以直接使用
…在我看来,这样更简洁。需要注意的是,`root` 的特异性与类相同,都是 0,1,0
很高兴能了解 BEM 格式的详细知识。很高兴知道它仍然在竞争中。
我对这些特异性数字感到非常困惑。这些有序元组是从哪里来的?在 https://webdev.ac.cn/learn/css/specificity/ 中,特异性有 1、10、100 等数字。
所以
的特异性有 11 点。
嗨,Tarun!
我认为,熟悉特异性评分的最佳方法是使用计算器。
我推荐这个:https://polypane.app/css-specificity-calculator/#selector=div%3Anot(.my-class)
注意,你的示例应该在这里预先加载。
市面上还有其他 CSS 特异性计算器,但我发现这个是最好的,因为它支持我在这篇文章中提到的所有 4 级选择器(有些计算器不支持)。
至于逗号分隔的格式,你可以直接将其视为一个普通数字。例如,
p a
的评分是 `0,0,2`。你可以将其读作“2”。
而
#thing p a
的评分是 `1,0,2`。你可以将其读作“102”。
数字越大,特异性就越高。
嗨,Liam,
我在这个旁注中有点迷路了
为什么使用伪类被认为是可以接受的,而 `card--featured .card-title` 就不行呢?
另外,我目前正在参与一个项目,我们正在考虑使用 `:hover` 这样的伪类,或者是否使用 `--hover` 修饰符会更好。你有什么专业意见吗?