样式方案的演化
要点
- 样式方案从「全局命名」走向「模块化/组件化」再到「原子化」,每一步都在降低耦合与认知成本。
- 原子化 CSS 的价值:更好的可组合性与摇树优化;风险在于约束失效后的 class 泛滥与设计漂移。它不是风口产物,而是长期向「更小粒度、更好约束」演进的结果。
- 多 runtime(Web/RSC/小程序)下,需要把 tokens 抽象到样式方案之外,避免在构建或运行时动态拼类。
组件库部分已拆为独立页面:组件库的演进。
样式方案时间轴
2010:Raw CSS / BEM / OOCSS,语义命名 + 手写层级。
2013:Sass/Less 变量与混入,BEM 体系化。
2015:CSS Modules 作用域隔离。
2017:CSS-in-JS,组件边界 + 运行时成本。
2020:Utility-first,JIT、摇树、tokens 对齐。
2024:Token/原子类跨 Web/RSC/小程序协作。
现场感:几段常见的「升级理由」
- BEM 项目重构时,遇到
.btn--primary在四个文件里被覆盖,且 hover 颜色不一致;引入 Utility-first 后把按钮拆成变体,类名归到cva工厂,审阅时一眼能看出状态组合。 - CSS-in-JS 方案在 SSR 下首屏注水 60KB,改为 Tailwind + tokens 后,首屏 CSS 控制在 8KB 以内,且通过
content精准扫描避免动态膨胀。 - Mini Program 场景 class 长度有限,原子类 + 预生成模板替换了动态拼接的 style 字符串,稳定性提高,构建速度更快。
各阶段优势 / 劣势 / 适用场景
| 阶段 | 核心优势 | 主要劣势 | 适用场景 | 常见坑 & 规避 |
|---|---|---|---|---|
| Raw CSS + BEM/OOCSS | 简单直观;命名带语义 | 全局污染、样式覆盖难排查;复用弱 | 小体量页面、低复杂度站点 | 命名规则不统一 → 制定前缀/分块;避免长选择器链 |
| 预处理器(Sass/Less) | 变量/混入/函数提高复用;BEM 更容易落地 | 仍是全局;易出现嵌套地狱;编译体积膨胀 | 需要一定复用,但没有组件边界要求 | 限制嵌套层级;lint 禁止 #id/深度选择器;保持色板集中 |
| CSS Modules | 作用域隔离,防止全局污染;类名可组合 | 样式与组件耦合,跨组件复用要额外抽象;难对齐统一 design token | 可发布组件库、需要隔离的中大型项目 | 建立 shared variables 文件;避免在组件内声明全局变量;主题切换需额外管线 |
| CSS-in-JS | 组件边界天然隔离;props 驱动样式;SSR 友好(视库而定) | 运行时开销;编译链复杂;类名可读性差;热更/缓存成本 | 需要强动态样式、主题切换、设计系统与组件强绑定 | 使用零运行时方案(vanilla-extract)或编译模式;监控包体;限定动态样式范围 |
| Utility-first(Tailwind/Uno) | 类名即样式,低认知切换;JIT/摇树;与 tokens 对齐;生态丰富 | 可读性与约束依赖团队规范;content 不精准会膨胀;动态类易失控 | 需要高迭代速度、设计体系对齐、组件组合化的前后端/多端项目 | 建立 tokens/variants 规范;统一 cn + merge;禁止字符串拼类;content 精准匹配;保留「推荐组合」文档 |
| Token + Headless 组件(shadcn/ui, reka-ui) | API 与样式解耦;通过 cva/tailwind-variants 集中管理变体;易被 AI/脚本生成 | 需要自建 design system;无约束时风格漂移 | 需要统一体验的多产品线、希望可插拔主题/品牌的团队 | 维护 tokens 表;评审类名;为 AI 提示加入黑名单/白名单;保留 merge/lint 校验链 |
迁移建议:如果已有 CSS Modules/组件库想拥抱原子化,可先将公共 tokens 抽出,再在 Headless 组件上叠加
cva/tailwind-variants,逐步替换局部样式。
深入阅读(按阶段拆分)
- Raw CSS / BEM / OOCSS
- Sass / Less 预处理
- CSS Modules 阶段
- CSS-in-JS 阶段
- Utility-first / Tailwind / UnoCSS
- Token 化与 Headless 组件
阶段代表性包速览与跳转
| 阶段 | 包 | 常见用法 | Demo/图表 |
|---|---|---|---|
| Raw CSS | normalize.css, Bootstrap, Bulma | 全局命名 + 组件 class | 本页 BEM 例子 + Reset 引入 |
| 预处理器 | Sass, Less, PostCSS, Stylus | 变量/混入、modifyVars 主题 | Sass 片段 & PostCSS 插件链 |
| CSS Modules | webpack/Vite modules, Next.js, vanilla-extract | 作用域哈希、零运行时 | 模块化 Card 组件 |
| CSS-in-JS | styled-components, Emotion, JSS, vanilla-extract | 运行时/编译期动态样式 | 主题 Button + 运行时 vs 编译期图 |
| Utility-first | Tailwind, Windi, UnoCSS, twin.macro | JIT 原子类、attributify、宏模式 | Tailwind 卡片 + Uno attributify + JIT 时序图 |
| Headless + tokens | Radix, Headless UI, shadcn/ui, tailwind-variants | tokens → variants → primitives | Radix Tabs、Menu Demo、tokens 流程图 |
决策树:选择样式方案
原子化 CSS 解决/未解决的问题
- ✅ 解决:
- 认知负担:类名即样式,无需跳转文件。
- 漂移与覆盖:减少全局样式泄漏;
content精准扫描,摇树删除未用类。 - 设计对齐:tokens 与 variants 让「设计 → 类名」有映射。
- ⚠️ 风险:
- 可读性:类名过长、无约束导致审阅困难。
- 一致性:不同人随意取值,导致色板/间距失控。
- 体积:动态类或 content 过宽会失去摇树收益。
- 🚫 不推荐使用的场景:
- 需要强隔离的可发布组件库(可用 CSS Modules/vanilla-extract)。
- 极简静态站点,对运行时无需求,模板类名冗长反而成本高。
- 无法建立 design token/规范的团队,原子类易失控。
与运行时/平台的适配
- Web/RSC:优先静态生成、
content精准匹配 server 侧模板;避免在服务器计算随机类名。 - 小程序/多端:注意 class length 限制;预生成静态类,不依赖动态模板拼接。
- SSR/HMR:Tailwind v4 JIT 足够快;UnoCSS 具更灵活的即时模式,但生态/插件差异需权衡。
