配了三年 CSS,每次第三方库样式打架都要靠 !important 强压——今天 @layer 把这件事从架构层彻底解了

配了三年 CSS,每次引入第三方组件库样式都要和它打架——它用 #app .sidebar .nav .btn.primary 这种嵌套选择器,我只能靠 !important 强压,压完三个月后没人记得为什么 half the styles have !important,这件事今天被 @layer 从架构层彻底解了。

specificity 的军备竞赛怎么打起来的

写 CSS 的都踩过这个坑:你的样式文件里写了一个 .btn,但页面上按钮颜色死活不对。打开 DevTools 一看,那个颜色来自 body #app .main-container .sidebar .btn.primary——第三方组件库写的,权重比你高出一截。

你有两个选择:要么写一个更长的选择器去对轰,要么在 .btn 后面加 !important。前者让选择器越来越长、越来越脆弱,后者开了个口子,之后每次被 !important 拦住的样式都要再压一个 !important 上去。半年之后,你的 stylesheet 里全是 !important,没人敢动,删一行就崩一个角落。

根子在于:CSS 的 cascade 优先级只看选择器权重和源码顺序,没有办法声明「我的代码比第三方库重要」。这个设计当年是合理的,但现代前端项目要接三个以上的 UI 库,就必然打起来。

@layer 做了什么

@layer 在 cascade 流程里插入了一个新步骤:层间优先级 > 选择器权重。

原来的 cascade 顺序是:来源和 !important → 选择器权重 → 源码顺序。

加了 @layer 之后:来源和 !important层间顺序 → 选择器权重 → 源码顺序。

这意味着:一个低权重的 .btn 写在高优先级层里,可以赢过高权重的 .sidebar .nav .btn.primary 写在低优先级层里。层的位置决定胜负,选择器权重只在同一层内比较

怎么用

第一步,在入口文件顶部声明层顺序:

@layer reset, base, third-party, components, utilities;

这个顺序一旦声明就锁定。reset 最低,utilities 最高。之后无论你在哪个文件写哪个层,这个顺序不变。

第二步,把第三方库扔进低优先级层:

/* 声明顺序 */
@layer reset, base, third-party, components, utilities;

/* 把第三方库整体放进 third-party 层 */
@import url("https://cdn.example.com/ui-library.css") layer(third-party);

/* 你的组件层永远赢 */
@layer components {
  .btn {
    font-family: var(--font-sans);
    letter-spacing: 0.025em;
    /* 赢了,不需要 !important,不需要更长的选择器 */
  }
}

现在,无论那个 UI 库的选择器有多长、权重有多高,你的 .btn 只要在 components 层里,就一定赢。这是两行 CSS,不是两屏选择器。

生产环境的层结构

中型以上项目推荐这个分层:

@layer reset,      /* normalize / modern-reset */
     tokens,       /* CSS custom properties,变量本身不参与权重竞争 */
     base,         /* h1, p, a 的默认样式 */
     third-party,  /* 所有外部库 */
     components,   /* 你的 UI 组件 */
     utilities,    /* 原子类,mt-4、text-center 这类 */
     overrides;    /* 应急补丁,少用 */

tokens 层比较特殊:里面放的是 CSS 变量,不是可渲染的规则,所以基本不和其他层竞争。它的作用是让变量定义有一个确定的位置,方便覆盖主题。

utilities 层放单用途原子类,比如 .hidden { display: none; }。这层的语义就是「我应该赢」,所以放在最上面是设计意图,不是 hack。

overrides 层是逃生舱。真实项目里总有那种「这个页面特殊,这个按钮颜色要变」的临时需求,放这层最合理,不要放 unlayered。

三个要注意的坑

unlayered 的 CSS 永远赢。这可能是最容易踩的坑:你在 @layer components 里写了很多样式,然后在文件底部随手加了一个 .card { background: gray; }——这个不在任何层里,它的优先级比所有层都高,会把你 components 层的 .card 覆盖掉。规则:一旦开始用 @layer,就把所有样式放进层里,随手写的紧急补丁放 overrides 层。

!important 在层间是反的。正常 CSS 里 !important 永远赢。层里不一样:!important 在低优先级层里仍然输给高优先级层的普通声明。这本来是合理的设计(让 CSS 库可以安全地用 !important,你的层仍然能赢),但第一次遇到会觉得很反直觉。

Layer 顺序由第一次声明决定。如果你先写了 @layer components, reset,然后又写了 @layer reset, base,顺序已经锁定了,后面再声明不会改变。常见 bug 是某个组件文件在主入口文件之前加载,先声明了一个翻转的顺序,然后主入口以为自己在控制顺序。

Tailwind v4 的情况

Tailwind v4 内部已经用 @layer 重写了。它的默认层顺序是 theme, base, components, utilities。如果你在项目里同时用 Tailwind v4 和自定义 CSS,正确做法是把自定义层嵌进去:

@layer reset, theme, base, custom-components, tailwind-utilities, overrides;

@import "tailwindcss";

@layer custom-components {
  .card {
    /* 这个在 Tailwind 的 base 之后、utilities 之前 */
    background: white;
    border-radius: 0.5rem;
    padding: 1.5rem;
  }
}

Tailwind 的 utilities 层永远赢,所以你在 custom-components 里写的样式会被 Tailwind 的原子类覆盖,这是预期行为。

下一步

如果你的项目现在有超过三个 !important,这件事值得认真对待。步骤:

  1. 在 main.css 顶部加 @layer reset, base, third-party, components, utilities, overrides;
  2. 把现有的 !important 按来源分类,哪个是第三方库的、哪个是自己写的
  3. 第三方库的 CSS 全部用 @import ... layer(third-party) 装进去
  4. 把自己项目的样式按组件/原子分类进对应层
  5. 删掉 !important,测试

浏览器支持已经是 98%+(Chrome 99+、Firefox 97+、Safari 15.4+),不需要 polyfill。@layer 不是 CSS 的新功能,是 CSS 终于把 cascade 优先级做成了显式声明。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 11 阅读