做前端这些年来,CSS 是唯一一个让我又爱又恨的技术。爱的是它足够灵活,恨的也是这份灵活——全局作用域、层叠优先级、选择器权重,这些特性在个人页面里是玩具般的存在,一旦进入大型项目,就成了级联混乱的根源。我参与过的团队里,几乎每周都会有人因为改一个 .title 的样式而影响其他页面的布局。正因为如此,CSS 的工程化方案才会从小圈子里的探究,逐步演变成每个前端团队都绕不开的必修课。
今天想跟你聊的,是当前前端社区讨论最集中的“CSS 三大主流方案”:以 BEM + 预处理器为代表的传统 CSS 方案、编译期完成隔离的 CSS Modules、以及完全由 JavaScript 驱动的 CSS-in-JS。这三种方案本质上都指向同一个问题——如何让全局性的 CSS 在组件化的世界里安全、高效地运行。不管你是刚接触组件化开发的新手,还是正在做技术选型的团队负责人,这篇文章都会结合我多年的实际项目经验,把每条路线的原理、代码形态、坑点以及选型依据一次讲透。
1. 为什么 CSS 需要“方案”,它到底在解决什么
在页面开发还没有组件化的年代,CSS 的全局特性并不是问题。一个 HTML 文件配一个或几个 CSS 文件,只要命名统一,很少出乱子。但组件化开发普及之后,场景彻底变了:同一个页面由几十上百个组件拼接,每个组件都有自己的样式文件,而这些文件里的类名仍然共享同一个全局命名空间。这意味着,你在任何组件里写下一个 .button,它都有可能误伤页面里其他组件的同名类。
1.1 全局命名空间才是万恶之源
CSS 中的每个选择器默认都是全局的,这一点和 JavaScript 的模块化完全不同。JS 里你 import 一个模块,它的内部变量和函数不会跑出去污染别的模块;CSS 没有这样的隔离机制。举个实际例子:A 组件里定义 .card { padding: 16px; },B 组件里也定义一个 .card { padding: 8px; },两个类名在同一个文档里同时存在时,谁的规则生效取决于加载顺序和选择器权重,而不是组件的嵌套深度。结果就是,页面样式在不知不觉中被改掉,排错时你根本想不到问题居然出在隔壁组件。
1.2 层叠模型里的“暗雷”不止一个
除了全局性,CSS 还牵扯到层叠和继承。一个元素的最终样式,是选择器优先级、源码顺序、!important、继承规则多方博弈后的结果。类选择器、ID 选择器、标签选择器……不同权重逐层累加,两个组件里同时命中一个元素时,稍不注意就会覆盖错。再加上 inline style 和 CSS 动画对样式的强制干预,排错难度直接翻倍。很多人把 CSS 项目难维护归结于“同事写得太烂”,但根子其实在语言本身的特性上。
1.3 三大方案的解题方向完全不同
要解决全局污染,思路其实就三条:第一条,通过命名规范让每个类名在全局范围内尽可能唯一,这是传统方案;第二条,在构建期给类名加上哈希指纹,把局部样式真正变成局部,这是 CSS Modules;第三条,把样式代码写进 JS 组件里,由运行时生成带唯一标识的样式规则,这是 CSS-in-JS。三条路线的底层逻辑各不相同,选型时不能只看表面 API 差异,要看它们对“隔离”这一核心问题的切入方式。
1.4 选型前先想清楚这几件事
在进入方案细节之前,建议你先回答三个问题:项目的生命周期有多长?团队里有多少人同时维护样式文件?首屏性能是不是核心指标?这三个问题的答案,会直接影响方案选择。后面每个方案的优缺点分析,都会围绕这三个问题展开。经验告诉我,很多团队把精力花在争论“哪个方案更现代”上,却忽略了自己项目的真实诉求,最后选了一个看起来很酷、但用起来处处别扭的方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 传统 CSS + 预处理器:最稳妥的基石路线
坦白说,现在很多技术文章一上来就把传统方案当成“落后的旧时代产物”,我不太认同。传统方案仍然是生产环境占有率最高、踩坑最少的一条线,理由很朴素:它没有额外运行时开销,不需要改造构建链,而且团队里每个人多多少少都会一点。
2.1 BEM:用命名约定让类名“人为唯一”
BEM 是 Block Element Modifier 的缩写,核心思想是把组件看成块(Block),块内部的元素叫 Element,状态或变体叫 Modifier,命名规则是 .block__element--modifier。比如一个搜索表单:
css复制/* 传统 BEM 写法 */
.search-form {}
.search-form__input {}
.search-form__input--active {}
.search-form__button {}
这段命名把作用域信息直接写进了类名里。别人看到 .search-form__input--active 就能立刻知道:它属于 search-form 块、是 input 元素、当前处于激活状态。类名天然趋近唯一,全局冲突的概率大幅下降。
不过我也见过不少团队觉得“用了 BEM 就万事大吉”,实际效果并不好。因为这类方案依赖人的自觉:只要有人图省事写了一个 .search-btn,或者不知道如何命名一个嵌套很深的子元素,规范就会在一次次妥协中失效。BEM 在实际项目里还有个难以拿捏的问题——粒度。块套块时,命名层级怎么处理?子元素的子元素也继续按照 __element 吗?写长了又长又丑,写短了又容易撞车。这些细节没有统一答案,只能靠团队内部约定,时间一长,很难保持一致。
2.2 预处理器:给样式表补上“编程能力”
Sass / LESS 的真正价值,不只是嵌套写法省事。变量、Mixins、函数、循环这些能力,把样式表从“复制粘贴改数值”升级成了“可参数的样式代码”。下面这段是典型的写法:
scss复制$primary-color: #1890ff;
$spacing-base: 8px;
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.search-form {
padding: $spacing-base * 2;
&__input {
border: 1px solid $primary-color;
@include flex-center;
&--active {
border-color: darken($primary-color, 10%);
}
}
}
不过我得提醒一句:嵌套层级尽量不要超过三层。这是我早期吃过亏后的教训。嵌套越深,编译出的选择器越长,权重越高,后续想覆盖越难。如果哪天真遇到需要四层以上的场景,更合理的做法是拆成独立的块,而不是继续往深处钻。
2.3 用新 CSS 特性给传统方案续命
传统方案不太“传统”的地方在于,CSS 原生能力这些年一直在增强。设计稿里的间距、颜色、字号,可以沉淀为 CSS 变量;Flex 解决一维布局,Grid 解决二维布局,已经成了日常布局的标配;文字渐变可以用 background-clip: text 实现;mask 可以剪裁出不规则的镂空效果。这些特性配合预处理器,足够覆盖绝大多数页面的视觉效果。
就拿 flex 布局子元素
