← 返回文章

CSS / 前端工程 / 样式架构

CSS 级联层实战:用 @layer 管住大型项目的样式优先级

项目刚开始时,样式覆盖通常很简单:后写的规则覆盖先写的规则。等到组件库、业务样式、工具类和历史补丁混在一起,事情就会变成另一副样子:为了让按钮变色,有人加一层父选择器;下一位开发者发现还不够,只好加 !important;半年后,没人敢删除任何一条规则。

CSS 级联层(Cascade Layers)提供了一种更稳定的解决方式。它通过 @layer 明确不同样式来源的优先顺序,让架构层级先于选择器权重参与级联。你不再需要靠“把选择器写得更长”表达业务优先级。

一、先弄清真正的级联顺序

当多个声明竞争同一个属性时,浏览器不会只比较 specificity。它会依次考虑来源与重要性、级联层、选择器权重、作用域接近度以及源码顺序。只有前面的条件打平,后面的比较才有机会生效。

这解释了为什么一个非常长的选择器仍可能输给另一层里的规则:层的优先级先于同一来源中的选择器权重。

可以在样式入口先声明层的顺序:

1
@layer reset, base, components, utilities, overrides;

对于普通声明,后声明的层优先级更高。上面的顺序意味着:

1
reset < base < components < utilities < overrides

随后把规则放进对应层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
}
}

@layer base {
body {
margin: 0;
color: #1f2328;
font-family: system-ui, sans-serif;
}
}

@layer components {
.button {
padding: 0.75rem 1rem;
border-radius: 0.75rem;
background: #20242c;
color: white;
}
}

@layer utilities {
.text-danger {
color: #b42318;
}
}

即使 .button 的规则更靠后、选择器看起来更“具体”,utilities 层的普通声明仍然可以按预先约定的架构覆盖它。

二、把第三方样式放进低优先级层

组件库经常带来高权重选择器。过去我们会在业务代码里复制它的结构,再写一个更长的选择器:

1
2
3
.checkout-page .form .vendor-button.primary {
background: #155eef;
}

更可维护的做法是导入时就把第三方 CSS 放进指定层:

1
2
3
4
5
6
7
8
9
@layer vendor, app;

@import url("vendor-ui.css") layer(vendor);

@layer app {
.primary-action {
background: #155eef;
}
}

只要业务元素拥有 .primary-action,app 层就能稳定覆盖 vendor 层,不必研究第三方库究竟嵌套了几层 DOM。以后组件库升级选择器,业务覆盖关系仍由层顺序保证。

这也是级联层最大的工程价值:把“谁应该覆盖谁”从偶然的选择器竞赛,变成入口文件里的公开契约。

三、注意未分层样式的特殊位置

一个容易踩坑的规则是:同一来源中的普通声明,未放进任何层的样式会排在所有已声明层之后,也就是拥有更高优先级。

1
2
3
4
5
6
7
8
9
@layer components {
.notice {
color: navy;
}
}

.notice {
color: crimson;
}

最终颜色是 crimson。这并不是因为第二条规则写在后面,而是因为它属于未分层的普通样式。

因此迁移旧项目时不要只把一半样式装进层里。常见策略是:

  1. 先声明完整层顺序。
  2. 把外部库放入 vendor 层。
  3. 把存量业务样式整体放入 legacy 层。
  4. 新组件进入 components,新工具类进入 utilities。
  5. 等覆盖关系稳定后,再逐步拆分 legacy。
1
@layer vendor, legacy, base, components, utilities, overrides;

如果大量旧规则仍留在未分层区域,它们会压过新层,迁移就会看起来“完全没生效”。

四、!important 的层顺序会反转

普通声明中,后面的层优先;!important 声明则会反转层顺序。最先声明的层,其 important 声明反而更强。

这种设计保护了底层约束。例如 reset 或安全相关规则如果必须使用 !important,后续层不能轻易改掉它。但它也意味着不能凭普通层顺序推断 important 的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
@layer base, components;

@layer base {
.button {
color: black !important;
}
}

@layer components {
.button {
color: white !important;
}
}

这里 base 层的 black 会胜出。

实践中仍应把 !important 当作例外。级联层解决的是架构优先级,不是鼓励把所有冲突都变成 important 冲突。需要无障碍保障、第三方内联样式兜底等明确理由时再使用,并在代码旁说明原因。

五、为项目设计少而稳定的层

一个可落地的结构通常不需要十几层:

1
@layer reset, tokens, base, components, utilities, overrides;
  • reset:盒模型、默认边距等标准化规则。
  • tokens:自定义属性和设计令牌。
  • base:元素级基础排版。
  • components:按钮、卡片、导航等组件。
  • utilities:单一职责的工具类。
  • overrides:明确且短期存在的业务覆盖。

设计令牌通常只是自定义属性声明,本身不一定需要高优先级。把它单独分层主要是帮助定位与治理。overrides 层则应定期清理;如果它不断增长,说明组件 API 或层划分仍有问题。

层还可以嵌套,但不要为了目录结构机械创建深层树:

1
2
3
4
5
@layer components {
@layer form {
.field { /* ... */ }
}
}

嵌套层会形成完整名称 components.form。只有当子层之间确实存在稳定的优先关系时才值得使用,否则简单的组件命名已经足够。

六、如何逐步迁移而不打乱现有页面

大型项目可以按下面的顺序落地:

  1. 从构建后的 CSS 中盘点 reset、第三方库、全局样式和工具类。
  2. 在唯一入口声明层顺序,避免各文件首次出现顺序不一致。
  3. 先包装第三方样式和新模块,不立即改写所有旧选择器。
  4. 对核心页面做截图回归,重点检查弹窗、表单和状态类。
  5. 删除已经不再需要的高权重父选择器和 !important
  6. 在代码规范中写明每层允许放什么,防止所有规则都进入 overrides。

如果项目使用 CSS Modules,局部类名可以减少命名冲突,但不会取消级联。组件内部状态、全局主题和工具类仍可能竞争属性,级联层依然有价值。

七、上线前检查清单

  • 层顺序是否只在入口处集中声明?
  • 第三方样式是否被放入明确的低优先级层?
  • 是否存在意外留在层外、压过所有层的普通规则?
  • important 声明是否理解了顺序反转?
  • overrides 是否有负责人和清理计划?
  • 关键页面是否做了桌面、移动端和深色主题回归?

级联层不会让 CSS 自动变简单,但它给了团队一套稳定的覆盖协议。选择器重新回到“描述元素”的职责,优先级交给架构管理。做到这一点,样式表才不会随着项目增长逐渐变成只能追加、不能修改的历史遗迹。

参考资料