CSS 层叠层 vs. BEM vs. 工具类:特异性控制详解

本文深入探讨了CSS特异性控制的三种主要方法:BEM命名法、工具类(如Tailwind)和CSS层叠层(@layer),分析它们的优缺点、适用场景及如何结合使用,帮助开发者更好地管理样式冲突。

CSS 层叠层 vs. BEM vs. 工具类:特异性控制

CSS 是不可预测的——而特异性往往是罪魁祸首。Victor Ayomipo 分析了为什么样式可能不按预期工作,以及为什么理解特异性比依赖 !important 标志更好。

CSS 是狂野的,真的非常狂野,而且棘手。但让我们具体谈谈特异性。

在编写 CSS 时,几乎不可能没有遇到过样式未按预期应用的挫败感——那就是特异性。你应用了一个样式,它起作用了,然后你尝试用另一个样式覆盖它,但……什么也没发生,它只是忽略了你。再次强调,这就是特异性。

当然,可以选择使用 !important 标志,但像所有开发人员一样,这总是有风险的,并且不鼓励使用。完全理解特异性比走那条路要好得多,否则你最终会与自己重要的样式作斗争。

特异性基础

许多开发人员以不同的方式理解特异性的概念。

特异性的核心思想是,浏览器使用的 CSS 层叠算法在两条或更多规则匹配同一元素时决定应用哪个样式声明。

想想看。随着项目的扩展,特异性挑战也会增加。假设开发者 A 添加了 .cart-button,然后按钮样式看起来可以在侧边栏中使用,但需要稍作调整。后来,开发者 B 添加了 .cart-button .sidebar,从那时起,任何应用于 .cart-button 的未来更改都可能被 .cart-button .sidebar 覆盖,就这样,特异性战争开始了。

我写 CSS 的时间足够长,见证了开发人员用来管理 CSS 特异性战争的不同策略。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* 传统方法 */
#header .nav li a.active { color: blue; }

/* BEM 方法 */
.header__nav-item--active { color: blue; }

/* 工具类方法 */
.text-blue { color: blue; }

/* 层叠层方法 */
@layer components {
  .nav-link.active { color: blue; }
}

所有这些方法反映了如何控制或至少维护 CSS 特异性的不同策略:

  • BEM:通过明确性来简化特异性。
  • 工具优先 CSS:通过保持原子性来绕过特异性。
  • CSS 层叠层:通过将样式组织成分层组来管理特异性。

我们将把这三者放在一起,看看它们如何处理特异性。

我与特异性的关系

我过去曾以为我完全理解了 CSS 特异性的全貌。比如内联样式大于 ID,ID 大于类,类大于标签。但是,阅读 MDN 关于 CSS 层叠如何真正工作的文档让我大开眼界。

有一个我在客户提供的旧代码库中处理的代码,看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* 遗留代码 */
#main-content .product-grid button.add-to-cart {
  background-color: #3a86ff;
  color: white;
  padding: 10px 15px;
  border-radius: 4px;
}

/* 这里还有 100 行其他代码 */

/* 我的新 CSS */
.btn-primary {
  background-color: #4361ee; /* 新品牌颜色 */
  color: white;
  padding: 12px 20px;
  border-radius: 4px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

看这段代码,.btn-primary 类根本没有机会对抗之前编写的任何特异性选择器链。就特异性而言,CSS 给第一个选择器的特异性分数是 1, 2, 1:一个 ID 点,两个类点,一个元素选择器点。同时,第二个选择器的分数是 0, 1, 0,因为它只包含一个类选择器。

当然,我有一些选择:

  • 我可以在 .btn-primary 的属性上使用 !important 来覆盖更强选择器中声明的属性,但一旦这样做,就要准备好到处使用它。所以,我宁愿避免它。
  • 我可以尝试更具体,但个人认为,这只是对下一个开发人员(甚至可能是我自己)的残忍。
  • 我可以更改现有代码的样式,但这会增加特异性问题:
1
2
3
#main-content .product-grid .btn-primary {
  /* 直接编辑样式 */
}

最终,我最终从头重写了整个 CSS。

当引入嵌套时,我尝试通过这种方式来控制特异性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.profile-widget {
  // ... 其他样式
  .header {
    // ... 头部样式
    .user-avatar {
      border: 2px solid blue;
      &.is-admin {
        border-color: gold; // 这变成了 .profile-widget .header .user-avatar.is-admin
      }
    }
  }
}

就这样,我无意中创建了高特异性规则。这就是我们如何轻易且自然地漂向特异性复杂性。

所以,为了节省自己很多这些问题,我有一个我一直遵守的原则:尽可能保持特异性低。如果选择器复杂性正在变成一个复杂的链,我会重新思考整个事情。

BEM:原始系统

块-元素-修饰符(简称 BEM)已经存在很长时间了。它是一种编写 CSS 的方法论系统,迫使你使每个样式层次结构明确。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 块 */
.panel {}

/* 依赖于块的元素 */
.panel__header {}
.panel__content {}
.panel__footer {}

/* 改变块样式的修饰符 */
.panel--highlighted {}
.panel__button--secondary {}

当我第一次体验 BEM 时,我认为它很棒,尽管有相反的意见认为它看起来很丑。我对双连字符或下划线没有问题,因为它们使我的 CSS 可预测且简化。

BEM 如何处理特异性

看看这些例子。没有 BEM:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* 特异性: 0, 3, 0 */
.site-header .main-nav .nav-link {
  color: #472EFE;
  text-decoration: none;
}

/* 特异性: 0, 2, 0 */
.nav-link.special {
  color: #FF5733;
}

使用 BEM:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* 特异性: 0, 1, 0 */
.main-nav__link {
  color: #472EFE;
  text-decoration: none;
}

/* 特异性: 0, 1, 0 */
.main-nav__link--special {
  color: #FF5733;
}

你看到 BEM 如何使代码看起来可预测,因为所有选择器都是平等的,从而使代码更易于维护和扩展。如果我想在 .main-nav 中添加一个按钮,我只需添加 .main-nav__btn,如果我需要一个禁用的按钮(修饰符),.main-nav__btn--disabled。特异性很低,因为我不必增加它或与层叠作斗争;我只是写一个新类。

BEM 的命名原则确保组件生活在隔离中,这对于 CSS 的一部分,即特异性部分,它是有效的,即 .card__title 类永远不会意外地与 .menu__title 类冲突。

BEM 的不足之处

我喜欢 BEM 的想法,但它并不完美,很多人注意到了:

  • 类名可能会变得非常长。
1
2
3
<div class="product-carousel__slide--featured product-carousel__slide--on-sale">
  <!-- 哎呀 -->
</div>
  • 可重用性可能不被优先考虑,这有点违背了原生 CSS 的意识形态。卡片内的按钮应该是 .card__button 还是重用全局 .button 类?对于前者,样式被复制,对于后者,BEM 的严格模型被打破。
  • 软件开发中的一个核心痛点开始成为现实——命名事物。我相信你已经知道这种挫败感。

BEM 很好,但有时你可能需要灵活处理它。混合系统(可能对核心组件使用 BEM,但在其他地方使用更简单的类)仍然可以保持所需的低特异性。

1
2
3
4
5
6
7
8
9
/* 没有 BEM 的基础按钮 */
.button {
  /* 按钮样式 */
}

/* 带有 BEM 的组件特定按钮 */
.card__footer .button {
  /* 次要覆盖 */
}

工具类:通过避免来处理特异性

这也称为原子 CSS。就其整体而言,它避免了特异性。

1
2
3
<button class="bg-red-300 hover:bg-red-500 text-white py-2 px-4 rounded">
  一个按钮
</button>

工具优先类背后的思想是每个工具类具有相同的特异性,即一个类选择器。每个类都是一个具有单一目的的微小 CSS 属性。

p-2?只是内边距,没有别的。text-red?文本颜色为红色。text-center?文本对齐。就像乐高积木一样,但用于样式。你将类堆叠在一起,直到获得所需的外观。

工具类如何处理特异性

工具类不解决特异性,而是将 BEM 的低特异性意识形态推向极致。几乎所有工具类都具有相同的最低可能特异性水平(0, 1, 0)。因此,覆盖变得容易;如果需要更多内边距,将 .p-2 提升到 .p-4

另一个例子:

1
2
3
<button class="bg-orange-300 hover:bg-orange-700">
  这个可以悬停
</button>

如果添加另一个类 hover:bg-red-500,顺序对 CSS 决定使用哪个很重要。所以,即使工具类避免了特异性,CSS 层叠的其他部分也会介入,即出现顺序,最后声明的匹配选择器获胜。

工具类的权衡

工具类最常见的问题是它们使代码看起来丑陋。坦率地说,我同意。但能够在不渲染的情况下想象组件的外观是无价的。

还有关于可重用性的争论,即你每次都在重复自己。但一旦发现重复发生,只需将该部分转换为可重用组件。它在特异性方面也有其真正的局限性:

  • 如果你的品牌颜色发生变化,这是一个全局变化,并且你深入代码库,你不能像原生 CSS 那样只更改一个并让其他跟随。
  • 由于原子工具类的行为方式,原生 CSS 中自然发生的父子关系被排除在外。

有些人认为 HTML 部分应该保留为标记,CSS 部分用于样式。因为现在有更多的标记需要扫描,如果你决定清理:

1
2
3
4
5
<!-- 太长 -->
<div class="p-4 bg-yellow-100 border border-yellow-300 text-yellow-800 rounded">

<!-- 更好? -->
<div class="alert-warning">

就这样,我们最终编写了 CSS。生命的循环。

根据我的工具类经验,它们最适合:

  • 速度:编写标记,样式化它,并迅速看到结果。
  • 可预测性:工具类完全按照它所说的做。

层叠层:通过设计来处理特异性

现在,这变得有趣了。BEM 提供结构,工具类获得速度,而 CSS 层叠层给了我们一些至关重要的东西:控制。

无论如何,层叠层(@layers)分组样式并声明组的顺序,而不管这些规则的特异性分数如何。

看一组独立的规则集:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
button {
  background-color: orange; /* 特异性: 0, 0, 1 */
}

.button {
  background-color: blue; /* 特异性: 0, 1, 0 */
}

#button {
  background-color: red; /* 特异性: 1, 0, 0 */
}

/* 无论如何,按钮是红色的 */

但使用 @layer,假设我想优先考虑 .button 类选择器。我可以塑造特异性顺序应该如何:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@layer utilities, defaults, components;

@layer defaults {
  button {
    background-color: orange; /* 特异性: 0, 0, 1 */
  }
}

@layer components {
  .button {
    background-color: blue; /* 特异性: 0, 1, 0 */
  }
}

@layer utilities {
  #button {
    background-color: red; /* 特异性: 1, 0, 0 */
  }
}

由于 @layer 的工作方式,.button 会获胜,因为组件层是最高优先级,即使 #button 具有更高的特异性。因此,在 CSS 甚至检查通常的特异性规则之前,首先会尊重层顺序。

你只需尊重 W3C 的人们,因为现在可以有意识地用简单的类覆盖 ID 选择器,甚至不使用 !important。迷人。

层叠层的细微差别

当我们谈论 CSS 层叠层时,有一些值得指出的事情:

  • 特异性仍然是游戏的一部分。
  • !important@layer 中的行为与预期不同(它们反向工作!)。
  • @layers 不是选择器特定的,而是样式属性特定的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@layer base {
  .button {
    background-color: blue;
    color: white;
  }
}

@layer theme {
  .button {
    background-color: red;
    /* 这里没有颜色属性,所以基础层的白色仍然适用 */
  }
}
  • @layer 很容易被滥用。我相信有一个开发人员有超过 20+ 层声明,已经变成了一个怪物。

比较所有三种方法

现在,对于 TL;DR 的朋友们,这里是三者的并排比较:BEM、工具类和 CSS 层叠层。

特性 BEM 工具类 层叠层
核心思想 命名空间组件 单一目的类 控制层叠顺序
特异性控制 低且平坦 完全避免 由于层优先的绝对控制
代码可读性 由于命名清晰的结构 如果不熟悉类名则不清晰 如果遵循层结构则清晰
HTML 冗长度 中等类名(可能变长) 许多小类,加起来很快 无直接影响,仅留在 CSS
CSS 组织 按组件 按属性 按优先级顺序
学习曲线 需要理解约定 需要知道工具名称 易于上手,但需要深入理解 CSS
工具依赖 纯 CSS 通常依赖第三方,如 Tailwind 原生 CSS
重构易度
最佳用例 设计系统 快速构建 需要覆盖的遗留代码或第三方代码
浏览器支持 所有 所有 所有(除 IE)

在这三者中,每个都有其最佳点:

  • BEM 最适合

    • 有一个需要一致性的清晰设计系统,
    • 有一个对 CSS 有不同理念的团队(BEM 可以是中间立场),
    • 样式不太可能在组件之间泄漏。
  • 工具类最适合

    • 你需要快速构建,如原型或 MVP,
    • 使用基于组件的 JavaScript 框架,如 React。
  • 层叠层最有效

    • 处理需要完全特异性控制的遗留代码库,
    • 需要集成第三方库或来自不同源的样式,
    • 处理大型、复杂的应用程序或具有长期维护的项目。

如果我必须选择或排名它们,我会选择工具类与层叠层,而不是使用 BEM。但这只是我!

它们的交集(如何协同工作)

在这三者中,层叠层应被视为协调者,因为它可以与其他两种策略一起工作。@layer 是 CSS 层叠架构的基本原则,不像 BEM 和工具类,它们是控制层叠行为的方法论。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 层叠层 + BEM */
@layer components {
  .card__title {
    font-size: 1.5rem;
    font-weight: bold;
  }
}

/* 层叠层 + 工具类 */
@layer utilities {
  .text-xl {
    font-size: 1.25rem;
  }
  .font-bold {
    font-weight: 700;
  }
}

另一方面,将 BEM 与工具类一起使用只会最终冲突:

1
2
3
4
<!-- 这感觉不对 -->
<div class="card__container p-4 flex items-center">
  <p class="card__title text-xl font-bold">似乎有什么不对</p>
</div>

我摊牌了:我是一个工具优先的开发人员。大多数工具类框架在幕后使用 @layer(例如,Tailwind)。所以,这两者已经在一起了。

但是,我讨厌 BEM 吗?一点也不!我用了很多,如果必要,我仍然会用。我只是发现命名事物是一项令人筋疲力尽的练习。

也就是说,我们都不同,你可能对什么感觉最好有相反的想法。这真的不重要,这就是这个网页开发空间的美妙之处。多条路线可以通向同一个目的地。

结论

所以,当比较 BEM、工具类和 CSS 层叠层时,是否有真正的“赢家”方法来控制层叠中的特异性?

首先,CSS 层叠层可以说是我们多年来获得的最强大的 CSS 功能。它们不应与 BEM 或工具类混淆,后者是策略而不是 CSS 功能集的一部分。

这就是为什么我喜欢将 BEM 与层叠层结合或工具类与层叠层结合的想法。无论哪种方式,想法是保持特异性低,并利用层叠层为这些样式设置优先级。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计