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. 坑四:不敢删的旧样式
现象:样式文件里有大量看起来没用的规则,但谁也不确定删掉会不会出事,于是越积越多。
修复步骤:
- 用 Chrome DevTools 的 Coverage 面板(Ctrl/Cmd + Shift + P,输入 Coverage)录制,红色部分是当前页面没用到的 CSS。
- Coverage 只统计当前页面和当前操作,不能据此直接删除。拿到可疑的类名后,在整个代码仓库里搜索它,包括 JS 中动态拼接的类名(如
'btn-' + type)。 - 确认没有引用后再删,并在测试环境逐页检查。
怎么避免再犯:组件的样式跟着组件文件走(单文件组件、CSS Modules),删组件时样式自然一起删,就不会产生孤儿规则。
排查思路:样式为什么不对
- 选中元素,在 Styles 面板看哪些规则被划掉了、哪条胜出。Chrome 会按
@layer分组显示规则,能直接看出层的先后。 - 鼠标悬停在选择器上可以看到它的优先级,判断是“比嗓门”输了还是“比顺序”输了。
- 只在生产环境出问题时,对比两个环境中样式文件的加载顺序(Network 面板筛选 CSS)。
- 怀疑样式泄漏时,在 Elements 面板看元素身上有哪些
data-v-xxx属性,判断它被哪些组件的 scoped 样式命中。
检查清单
- [ ] 样式按 tokens → reset → base → layout → components → utilities 分层
- [ ] 用
@layer声明层的顺序,第三方样式放在最前面的层 - [ ] 组件类名全局唯一,不依赖父级上下文
- [ ] 没有两份文件定义同名、同优先级的规则
- [ ] 不新增
!important,覆盖问题先考虑分层 - [ ] 组件样式跟随组件文件,删除组件时一起删除
延伸阅读
- BEM 命名规范:优先级的计算方法
- CSS 变量与主题系统:tokens 层怎么设计
- 现代 CSS 选择器:用
:where()写零优先级的默认样式