Skip to content

CSS 架构与组织 ​

项目刚开始时,CSS 怎么写都行。半年后,问题会集中爆发:

  • 改一个按钮的样式,十几个页面跟着变;
  • 想覆盖组件库的样式,写了三层选择器还是不生效;
  • 本地开发一切正常,打包上线后某些样式“随机”失效;
  • 样式文件越来越大,但没人敢删任何一行。

CSS 架构要解决的就是这些问题:让每一次改动的影响范围可控。

读完这篇你会

  • 说清浏览器按什么顺序决定哪条样式生效
  • 用分层的方式组织样式,并用 @layer 原生实现分层
  • 解决第三方样式覆盖、加载顺序不一致、scoped 泄漏和旧样式清理问题

1. 原理:谁说了算——层叠规则 ​

同一个元素被多条规则命中时,浏览器按下面的顺序裁决,前一条分出胜负就不再往下比:

text
1. 重要性:带 !important 的声明胜出
2. 层(@layer):后声明的层胜过先声明的层,与选择器写法无关
3. 优先级:比较 (ID 个数, 类/属性/伪类个数, 标签个数)
4. 出现顺序:以上都相同时,写在后面的胜出

可以把它想成公司里的决策:职级(层)高的人一句话就能拍板,不管职级低的人嗓门(优先级)多大;职级相同才比嗓门;嗓门也一样大,就听最后发言的。

大部分 CSS 的混乱,都来自只靠第 3、4 条在“比嗓门”:为了覆盖别人就加选择器、加 !important,越来越大声。好的架构是把裁决权交给更高的第 2 条:用分层决定谁优先,而不是靠选择器打架。

2. 分层组织:从通用到具体 ​

把样式按“影响范围从大到小”分层,越往后越具体,后面的层可以覆盖前面的层:

层放什么例子
tokens设计令牌(变量)--color-primary、--space-4
reset统一浏览器默认样式box-sizing: border-box
base元素默认样式body、a、h1 的字体颜色
layout不带视觉效果的布局.container、.stack、.grid
components具体组件.article-card、.navbar
utilities单一用途的辅助类.sr-only、.text-center

这套思路来自 ITCSS,不必照搬它的全部术语,记住两条就够:越靠后越具体;后面的层可以用前面的东西,前面的层不依赖后面的层。改主题只动 tokens,改卡片只动 components,互不牵连。

用 @layer 把分层变成浏览器能懂的规则 ​

过去分层只能靠文件的引入顺序和团队自觉。现在可以用 @layer 直接告诉浏览器层的先后,主流浏览器从 2022 年起都已支持:

css
/* 第一行声明层的顺序:越靠后优先级越高 */
@layer reset, base, layout, components, utilities;

@layer base {
  a { color: var(--color-primary); }
}

@layer components {
  .nav-link { color: var(--color-text); }
}

@layer utilities {
  .text-muted { color: var(--color-text-muted); }
}

有了层,.text-muted(utilities 层)一定能覆盖 .nav-link(components 层),不需要比较选择器长短,也不需要 !important。

使用 @layer 要记住两个反直觉的细节:

  • 没有放进任何层的样式,优先级高于所有层。引入层之后,零散的老样式会“压过”你精心分好的层,迁移时要么把它们也放进层,要么心里有数。
  • !important 会把层的顺序反过来:越早的层里的 !important 越强。这正是为了让 reset 之类的底层规则在必要时能守住底线。

小项目不必建七个目录。把 tokens/reset/base 合在一个 base.css 里也完全可以,关键是保持方向:通用的在前,具体的在后。

3. 全局样式和组件样式怎么分 ​

判断标准只有一个:这段样式会被多个页面或组件复用吗?

类型放在哪
令牌、reset、元素默认样式全局
通用布局、工具类全局
某个组件的外观组件自己的样式(scoped / CSS Modules / BEM)
只有一个页面用的特殊布局页面自己的样式

一个健康的组件应该和位置无关:把它挪到任何页面、任何容器里,外观都不变。如果它必须放在 .page-home 下才正常,说明它偷偷依赖了外部样式。

4. 样式隔离的几种方案 ​

原生 CSS 是全局的,下面这些方案用不同方式给组件造出“作用域”:

方案原理注意
BEM 命名类名带组件前缀,靠约定避免冲突不需要工具,但依赖团队纪律
Vue scoped编译时给元素加 data-v-xxx 属性,选择器带上它子组件的根元素也会被父组件样式命中
CSS Modules编译时把类名改成唯一的哈希模板里通过 styles.xxx 引用
CSS-in-JS由 JS 生成唯一类名运行时方案有性能成本

5. 坑一:覆盖不了第三方组件库的样式 ​

现象:想改组件库按钮的内边距,写了 .my-page .el-button,不生效;再加一层选择器,还是不生效,最后只能 !important。

原因:组件库的选择器往往比较长(为了自身稳定),你的选择器优先级不够;就算够了,样式文件的加载顺序也可能让它在你之后生效。

修复:把第三方样式放进一个更早的层,你自己的样式无论怎么写都能覆盖它:

css
@layer vendor, base, components;

/* 第三方样式整体放进 vendor 层 */
@import url('some-ui-lib/dist/index.css') layer(vendor);

@layer components {
  .el-button { padding: 0.5rem 1rem; } /* 一层类名就能覆盖 */
}

如果用的是 Vue scoped,改子组件内部元素需要用 :deep(.el-button)。但 :deep() 用多了,就又回到了“跨组件随意改样式”的老路,能通过组件参数或 CSS 变量定制的,优先用那种方式。

6. 坑二:本地正常,上线后样式随机失效 ​

现象:开发环境里按钮是蓝色的,生产环境里变成了灰色;而且不是所有页面都这样,只有从某些路由进入时才出现。

原因:两条优先级完全相同的规则在“打架”,胜负取决于谁后加载(层叠规则第 4 条)。开发环境里样式按模块顺序注入;生产环境做了代码分割,CSS 被拆成多个文件,按用户访问路由的顺序加载,于是“谁在后面”变得不确定。

css
/* button.css */
.button { background: var(--color-primary); }

/* legacy-form.css(某个页面的样式) */
.button { background: var(--gray-200); } /* 同名、同优先级 */

修复:

  • 不要让两个文件定义同名的类,页面里需要不同外观时用修饰符:.button--muted;
  • 用 @layer 明确先后,不依赖加载顺序。

怎么避免再犯:组件类名全局唯一(BEM 或 CSS Modules),“谁覆盖谁”交给层来决定。

7. 坑三:父组件的 scoped 样式漏进了子组件 ​

现象:父组件里写了 .wrapper { padding: 24px }(scoped),结果子组件的根元素也多出了 24px 的内边距。

原因:Vue 的 scoped 机制会给子组件的根元素同时加上父组件的 data-v-xxx 属性。这是故意设计的,方便父组件控制子组件的布局。但如果子组件的根元素恰好也用了 wrapper 这个类名,就会被父组件的样式命中。

修复:类名带上组件前缀(.order-list__wrapper),避免通用名字;或者让父组件只控制子组件的外部布局(margin、宽度),不碰内部样式。

8. 坑四:不敢删的旧样式 ​

现象:样式文件里有大量看起来没用的规则,但谁也不确定删掉会不会出事,于是越积越多。

修复步骤:

  1. 用 Chrome DevTools 的 Coverage 面板(Ctrl/Cmd + Shift + P,输入 Coverage)录制,红色部分是当前页面没用到的 CSS。
  2. Coverage 只统计当前页面和当前操作,不能据此直接删除。拿到可疑的类名后,在整个代码仓库里搜索它,包括 JS 中动态拼接的类名(如 'btn-' + type)。
  3. 确认没有引用后再删,并在测试环境逐页检查。

怎么避免再犯:组件的样式跟着组件文件走(单文件组件、CSS Modules),删组件时样式自然一起删,就不会产生孤儿规则。

排查思路:样式为什么不对 ​

  1. 选中元素,在 Styles 面板看哪些规则被划掉了、哪条胜出。Chrome 会按 @layer 分组显示规则,能直接看出层的先后。
  2. 鼠标悬停在选择器上可以看到它的优先级,判断是“比嗓门”输了还是“比顺序”输了。
  3. 只在生产环境出问题时,对比两个环境中样式文件的加载顺序(Network 面板筛选 CSS)。
  4. 怀疑样式泄漏时,在 Elements 面板看元素身上有哪些 data-v-xxx 属性,判断它被哪些组件的 scoped 样式命中。

检查清单 ​

  • [ ] 样式按 tokens → reset → base → layout → components → utilities 分层
  • [ ] 用 @layer 声明层的顺序,第三方样式放在最前面的层
  • [ ] 组件类名全局唯一,不依赖父级上下文
  • [ ] 没有两份文件定义同名、同优先级的规则
  • [ ] 不新增 !important,覆盖问题先考虑分层
  • [ ] 组件样式跟随组件文件,删除组件时一起删除

延伸阅读 ​

为复用而记录,为理解而整理。