CSS / 前端工程 / 样式架构
CSS 级联层实战:用 @layer 管住大型项目的样式优先级
项目刚开始时,样式覆盖通常很简单:后写的规则覆盖先写的规则。等到组件库、业务样式、工具类和历史补丁混在一起,事情就会变成另一副样子:为了让按钮变色,有人加一层父选择器;下一位开发者发现还不够,只好加 !important;半年后,没人敢删除任何一条规则。
CSS 级联层(Cascade Layers)提供了一种更稳定的解决方式。它通过 @layer 明确不同样式来源的优先顺序,让架构层级先于选择器权重参与级联。你不再需要靠“把选择器写得更长”表达业务优先级。
一、先弄清真正的级联顺序
当多个声明竞争同一个属性时,浏览器不会只比较 specificity。它会依次考虑来源与重要性、级联层、选择器权重、作用域接近度以及源码顺序。只有前面的条件打平,后面的比较才有机会生效。
这解释了为什么一个非常长的选择器仍可能输给另一层里的规则:层的优先级先于同一来源中的选择器权重。
可以在样式入口先声明层的顺序:
1 | @layer reset, base, components, utilities, overrides; |
对于普通声明,后声明的层优先级更高。上面的顺序意味着:
1 | reset < base < components < utilities < overrides |
随后把规则放进对应层:
1 | @layer reset { |
即使 .button 的规则更靠后、选择器看起来更“具体”,utilities 层的普通声明仍然可以按预先约定的架构覆盖它。
二、把第三方样式放进低优先级层
组件库经常带来高权重选择器。过去我们会在业务代码里复制它的结构,再写一个更长的选择器:
1 | .checkout-page .form .vendor-button.primary { |
更可维护的做法是导入时就把第三方 CSS 放进指定层:
1 | @layer vendor, app; |
只要业务元素拥有 .primary-action,app 层就能稳定覆盖 vendor 层,不必研究第三方库究竟嵌套了几层 DOM。以后组件库升级选择器,业务覆盖关系仍由层顺序保证。
这也是级联层最大的工程价值:把“谁应该覆盖谁”从偶然的选择器竞赛,变成入口文件里的公开契约。
三、注意未分层样式的特殊位置
一个容易踩坑的规则是:同一来源中的普通声明,未放进任何层的样式会排在所有已声明层之后,也就是拥有更高优先级。
1 | @layer components { |
最终颜色是 crimson。这并不是因为第二条规则写在后面,而是因为它属于未分层的普通样式。
因此迁移旧项目时不要只把一半样式装进层里。常见策略是:
- 先声明完整层顺序。
- 把外部库放入 vendor 层。
- 把存量业务样式整体放入 legacy 层。
- 新组件进入 components,新工具类进入 utilities。
- 等覆盖关系稳定后,再逐步拆分 legacy。
1 | @layer vendor, legacy, base, components, utilities, overrides; |
如果大量旧规则仍留在未分层区域,它们会压过新层,迁移就会看起来“完全没生效”。
四、!important 的层顺序会反转
普通声明中,后面的层优先;!important 声明则会反转层顺序。最先声明的层,其 important 声明反而更强。
这种设计保护了底层约束。例如 reset 或安全相关规则如果必须使用 !important,后续层不能轻易改掉它。但它也意味着不能凭普通层顺序推断 important 的结果。
1 | @layer base, components; |
这里 base 层的 black 会胜出。
实践中仍应把 !important 当作例外。级联层解决的是架构优先级,不是鼓励把所有冲突都变成 important 冲突。需要无障碍保障、第三方内联样式兜底等明确理由时再使用,并在代码旁说明原因。
五、为项目设计少而稳定的层
一个可落地的结构通常不需要十几层:
1 | @layer reset, tokens, base, components, utilities, overrides; |
reset:盒模型、默认边距等标准化规则。tokens:自定义属性和设计令牌。base:元素级基础排版。components:按钮、卡片、导航等组件。utilities:单一职责的工具类。overrides:明确且短期存在的业务覆盖。
设计令牌通常只是自定义属性声明,本身不一定需要高优先级。把它单独分层主要是帮助定位与治理。overrides 层则应定期清理;如果它不断增长,说明组件 API 或层划分仍有问题。
层还可以嵌套,但不要为了目录结构机械创建深层树:
1 | @layer components { |
嵌套层会形成完整名称 components.form。只有当子层之间确实存在稳定的优先关系时才值得使用,否则简单的组件命名已经足够。
六、如何逐步迁移而不打乱现有页面
大型项目可以按下面的顺序落地:
- 从构建后的 CSS 中盘点 reset、第三方库、全局样式和工具类。
- 在唯一入口声明层顺序,避免各文件首次出现顺序不一致。
- 先包装第三方样式和新模块,不立即改写所有旧选择器。
- 对核心页面做截图回归,重点检查弹窗、表单和状态类。
- 删除已经不再需要的高权重父选择器和
!important。 - 在代码规范中写明每层允许放什么,防止所有规则都进入 overrides。
如果项目使用 CSS Modules,局部类名可以减少命名冲突,但不会取消级联。组件内部状态、全局主题和工具类仍可能竞争属性,级联层依然有价值。
七、上线前检查清单
- 层顺序是否只在入口处集中声明?
- 第三方样式是否被放入明确的低优先级层?
- 是否存在意外留在层外、压过所有层的普通规则?
- important 声明是否理解了顺序反转?
- overrides 是否有负责人和清理计划?
- 关键页面是否做了桌面、移动端和深色主题回归?
级联层不会让 CSS 自动变简单,但它给了团队一套稳定的覆盖协议。选择器重新回到“描述元素”的职责,优先级交给架构管理。做到这一点,样式表才不会随着项目增长逐渐变成只能追加、不能修改的历史遗迹。