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

本文深入探讨CSS特异性控制的三种主流方案:BEM方法论、工具类原子化方案以及CSS层叠层技术,通过具体代码示例分析各自的优劣和适用场景,帮助开发者更好地管理样式冲突。

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 ideology。卡片内的按钮应该是 .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;
    /* 这里没有颜色属性,所以基础层的白色仍然适用 */
  }
}

@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 吗?一点也不!我用了很多,如果必要,仍然会用。我只是发现命名事物是 exhausting 练习。

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

结论

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

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

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

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