前端样式方案演进与选型
从全局样式表、BEM、CSS Modules、预处理器到 CSS-in-JS,梳理前端样式方案演进背后要解决的作用域、复用与动态性问题,并给出工程选型思路。
[!info] related notes
- 所属 MOC: CSS MOC
- 相关概念: CSS, BEM 命名规范, CSS Modules, Sass / SCSS, CSS-in-JS
- 易混淆概念: PostCSS 详解, tailwind, unocss
- 相关资源:
前端样式方案演进与选型
范围
这篇笔记关注的不是单个 CSS 特性,而是前端项目在样式工程化上常见的几条路线:
- 全局样式表
- BEM
- CSS Modules
- Sass / Less 等预处理器
- CSS-in-JS
- Atomic CSS / Utility-First CSS
- 原生 CSS 近年的能力回升
它回答的核心问题是:
- 样式为什么会越来越难维护
- 不同方案分别在解决什么
- 现代项目里该如何组合,而不是迷信单一方案
为什么要放在一起理解
前端样式方案的演进,本质上不是“新方案淘汰旧方案”的简单历史,而是围绕三个长期存在的问题不断补洞:
- 作用域:这段样式会不会污染不该影响的地方
- 复用:相同设计规则如何稳定复用,而不是复制粘贴
- 动态性:样式如何响应状态、主题、平台和环境变化
如果把这三件事拆开看,就容易误解每个方案的边界。
例如:
- 预处理器增强了表达能力,但不解决作用域。
- CSS Modules 解决了作用域,但不自动回答如何设计组件 API。
- CSS-in-JS 强化了动态样式,但会带来运行时和 SSR 成本。
依赖路径 / 调用链 / 演进链
1. 全局样式表:最自然,也最容易失控
CSS 最初服务的是文档样式化,不是大型组件化应用。
因此它天然是全局命名空间:
.title {}
.button {}
只要 DOM 匹配,规则就会生效。问题在于项目变大后,开发者很难知道一个选择器到底影响了哪些地方,于是:
- 命名冲突变多
- 删除旧样式越来越不敢动
- CSS 逐渐变成 append-only 的历史堆积
全局样式并没有消失,但更适合承载:
- reset / normalize
- 全局变量
- 字体与排版基线
- design tokens 的根定义
2. BEM:用命名约定模拟局部作用域
BEM 的核心思路是:既然 CSS 没有模块机制,那就通过结构化类名人为制造边界。
它解决的是:
- 全局类名太短太泛,容易撞名
- 组件内部结构不够清晰
- 团队协作时难以仅从类名判断归属
但 BEM 仍然是软约束:
- 依赖团队纪律
- 不能阻止别人写新的全局污染
- 类名可能变得很长
所以它更像“工程约定”,不是“技术隔离”。
3. CSS Modules:把作用域隔离交给构建工具
CSS Modules 把“局部作用域”从人工纪律推进到构建期自动化。
开发者仍写普通 CSS,但构建工具会:
- 解析类名
- 生成唯一标识
- 输出映射对象
于是两个组件都写 .title 也不会冲突。
这一步的变化非常关键,因为样式终于可以和组件的删除、重命名、拆分形成更紧的耦合关系。
4. 预处理器:解决表达能力,而不是作用域
Sass / Less 的核心价值是让 CSS 更容易组织和复用:
- 变量
- mixin
- 嵌套
- 函数
- 循环和条件
它解决的是“怎么更高效地写”,而不是“这段样式作用于哪里”。
因此预处理器经常和别的方案配合:
- 全局 CSS + BEM + SCSS
- CSS Modules + SCSS
5. CSS-in-JS:把样式纳入组件逻辑系统
CSS-in-JS 更进一步,不再把样式只看成外部静态资源,而是把它纳入组件状态和主题系统。
它最适合处理这些问题:
- props 决定样式
- 多主题切换
- 设计系统里的复杂 variant 组合
- 样式与组件 API 强绑定
但它也把一部分样式复杂度从 CSS 层搬到了 JS 运行时和工程配置层。
6. Atomic CSS / Utility-First:从“写组件类名”转向“组合约束好的工具类”
Tailwind、UnoCSS 代表的是另一条分支。
它不强调给每个组件发明新的语义类名,而是把设计系统暴露成一套可组合的工具类。
这样做的收益是:
- 复用更强
- 死 CSS 更少
- 命名成本更低
- 设计约束更统一
它解决的是“命名与复用成本”,不是关系链上某个单点的升级版。
7. 原生 CSS 的反向进化
近年的原生 CSS 已经比过去强很多:
- CSS variables
- container queries
- cascade layers
- 原生 nesting
这意味着一些过去必须依赖预处理器或 JS 的能力,正在重新回到浏览器原生层。
所以现代趋势更像:
- 能交给 CSS 的,就尽量交给 CSS
- 真正和组件状态强耦合的,再交给 JS
对比与易混淆点
这几条路线分别主要解决什么
- 全局样式表:基础样式、全局规则、主题根变量
- BEM:命名冲突和结构语义
- CSS Modules:组件级作用域隔离
- Sass / Less:变量、复用、组织和表达能力
- CSS-in-JS:样式与状态、主题、组件 API 的深度绑定
- Atomic CSS:约束化复用和快速组合
三个核心问题和各方案的对应关系
- 作用域
- 全局 CSS:几乎没有隔离
- BEM:人工隔离
- CSS Modules:构建期隔离
- CSS-in-JS:组件级隔离
- 复用
- 全局 CSS:公共类
- Sass / Less:变量、mixin、函数
- Atomic CSS:工具类组合
- 设计系统:tokens、组件 API、主题对象
- 动态性
- 传统 CSS:切换 class
- CSS variables:运行时变量切换
- CSS Modules:class 组合
- CSS-in-JS:props / state / theme 直接驱动
容易混淆的边界
- 预处理器不等于模块化。
- CSS Modules 不等于“样式逻辑就自动设计好了”。
- CSS-in-JS 不等于“任何项目都更先进”。
- Tailwind 不等于“只是把样式写回 HTML”,它更像设计系统的 API。
工程选型建议
小型页面或静态站点
更适合:
- 全局 CSS
- 少量 BEM
因为问题规模还没大到需要引入复杂样式系统。
中大型 React / Vue 业务系统
通常更稳妥的是:
- 全局样式层承载 reset 和 tokens
- CSS Modules 承载普通组件样式
- Sass / Less 补表达能力
这套组合在性能、学习成本和协作之间比较平衡。
组件库或设计系统
更可能需要:
- CSS variables
- 编译时或运行时 CSS-in-JS
- 明确的 variant API
因为这类场景更强调主题、变体和跨项目复用。
高性能、SSR、复杂前台应用
应谨慎使用运行时 CSS-in-JS。
通常更偏向:
- CSS Modules
- 原生 CSS variables
- 编译时 CSS-in-JS
- Atomic CSS
因为这类项目更敏感于首屏性能、样式注入顺序和 hydration 稳定性。
一条更成熟的分层思路
成熟团队往往不会只押注某一种方案,而是分层组合:
- 全局层:reset、排版基线、字体、主题根变量
- 基础层:design tokens、CSS variables
- 组件层:CSS Modules 或 CSS-in-JS
- 复用层:组件库、utility classes、mixin
- 动态层:props、state、theme、运行时变量
- 工程层:lint、构建工具、规范和类型系统
这条演进线的真正方向,不是“全部换成最新方案”,而是从全局、松散、靠经验,逐步走向局部、模块化、自动化和组件化。