CSS 特异性控制:层叠层、BEM 与工具类对比

本文深入探讨了CSS特异性控制的三种主流方法:BEM命名规范、工具类(原子CSS)和CSS层叠层(@layer)。通过代码示例和对比分析,揭示了每种策略在样式管理、代码可读性和维护性上的优缺点,帮助开发者根据项目需求选择合适方案。

CSS 特异性控制:层叠层、BEM 与工具类对比

CSS 是不可预测的——而特异性往往是罪魁祸首。Victor Ayomipo 剖析了样式为何不按预期生效的原因,并指出理解特异性比依赖 !important 标志更为重要。

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

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

当然,可以选择使用 !important 标志,但像所有前辈开发者一样,这总是有风险的,并且不被鼓励。完全理解特异性比走那条路要好得多,否则你最终会和自己加 !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 > 类 > 标签那样。但是,阅读 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 得 1 分,两个类得 2 分,元素选择器得 1 分。同时,第二个选择器的得分是 0, 1, 0,因为它只包含一个类选择器。

当然,我有一些选择:

  1. 我可以在 .btn-primary 的属性上使用 !important 来覆盖在更强选择器中声明的属性,但一旦这样做,就要准备好到处使用它。所以,我宁愿避免。

  2. 我可以尝试变得更具体,但就个人而言,那只是对下一个开发者(甚至可能是我自己)的残忍。

  3. 我可以更改现有代码的样式,但那是在增加特异性问题:

    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 层叠层给了我们一些至关重要的东西:控制。

总之,层叠层 (@layer) 对样式进行分组,并声明这些组应该遵循的顺序,而不管这些规则的特异性得分如何。

看一组独立的规则集:

 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 中的行为与预期不同(它们反向工作!)。

  • @layer 不是选择器特定的,而是样式属性特定的。

     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;
        /* 这里没有 color 属性,所以 base 层的 white 仍然适用 */
      }
    }
    
  • @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 吗?一点也不!我用了很多,如果必要的话仍然会用。我只是发现命名事物是一项令人筋疲力尽的练习。

话虽如此,我们都不同,你可能对你认为什么感觉最好有相反的想法。这真的不重要,这就是这个 Web 开发领域的美丽之处。多条路线可以通向同一个目的地。

结论

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

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

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

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