ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CSS框架怎么选:Tailwind、Bootstrap等五强对比与场景决策

CSS框架怎么选:Tailwind、Bootstrap等五强对比与场景决策

1. 先把"排名"这件事的口径掰开:五个名字分别赢在哪张表上

每年聊 CSS 框架排名,最容易发生的一幕是:两个人争得面红耳赤,最后一对数据源,发现一个在看 GitHub Star,一个在看 npm 下载量,还有一个在看自己公司内部技术栈。所以这篇我不想直接甩一个"1 到 5"就完事,而是先把口径讲清楚,再说 2024 年真正值得占用你技术选型会议时间的五个框架:Tailwind CSS、Bootstrap、Bulma、Pico CSS、UnoCSS。

这不是一份"谁最强"的榜单,而是一份"谁在什么位置上"的画像。我自己带过几个从零起步的前端项目,也接手过几个跑了五六年的老系统,越往后越确认一件事:框架没有绝对的好坏,只有"和你的项目寿命、团队构成、交付节奏咬不咬得上"。一个三人小队三个月要上线的营销站,和一个二十人维护五年的中后台,选同一个框架的概率其实很低。

1.1 三种"排名"背后的数据来源差异,决定了榜单会长得完全不一样

先看三种常见的排名依据,它们各自会推出不同的名次,理解这一点比记住名次重要得多。

排名口径主要看什么容易高估谁容易低估谁
社区热度(Star、讨论量、教程数)开发者关注度、话题性新锐工具、概念型框架老牌稳定但话题度低的框架
包管理器下载量实际安装行为被大量模板/脚手架间接依赖的框架通过 CDN 直接引入、不装包的老项目
生产环境使用率真实上线的站点建站工具、CMS 主题自带默认样式的框架内部系统、后台管理里的私有选择

Star 数是最容易失真的一项。一个框架可能因为一篇爆款文章涨几千 Star,但生产环境里根本没人用;反过来,某些框架在统计数据里几乎看不到,是因为大量项目直接引 CDN 或者用了老版本,压根不走包管理器。所以我看排名时习惯把三张表叠在一起看交集——交集里的名字,才是"确实有人在真实项目里用"的。

还有一个隐含维度经常被忽略:框架的维护活跃度曲线。2024 年这个时间点上,有几个曾经很响的名字(比如一些基于老版本 jQuery 生态的 UI 框架、以及几年没发版的语义化框架)已经从"候选名单"里掉出去了。选型时如果只看教程数量,很容易一脚踏进一个两年没更新的坑里,等到要升级构建工具链时才发现没人跟进。

1.2 2024 年这五个名字为什么站在台前

把这五个放在一起,是因为它们恰好覆盖了当下主流的五种设计哲学,而不是因为它们分数接近。

Tailwind CSS代表原子化(utility-first)。它的核心主张是"不要再给样式起名字",把样式拆成最小粒度的类直接写在标签上。2024 年它几乎是新项目默认选项,我在多个项目里见到的事实是:只要团队里有人用过一次,就很难再回到手写 BEM 的日子。

Bootstrap代表组件化 + 预设设计语言。它是这一代人认识前端的第一课,5.x 系列把 jQuery 彻底剥离、引入 CSS 变量和暗色模式之后,在"要快、要稳、要能应付客户审美"的场合依然是性价比之王。

Bulma代表纯 CSS 的组件化。它最大的特点是不含一行 JavaScript,所有组件只提供样式和状态类,交互自己写。这个选择在 2024 年反而变成了优势——当你的项目用了 Vue、React、Svelte 甚至原生 Web Components 时,不需要为了一个下拉菜单再引入一套 JS 逻辑。

Pico CSS代表语义化 / classless。它的思路是:你把 HTML 写对(用<article>、<nav>、<form>这些语义标签),它负责让页面直接好看。适合文档站、原型、内部小工具。

UnoCSS代表引擎化。它不预设一套固定的类名集合,而是把"从什么语法生成什么样式"做成可插拔的预设,你可以同时兼容 Tailwind 语法、属性化写法、图标、甚至自定义规则。

1.3 一张横向对照表,看清各自的性格

选型会上最有效的动作往往不是讲道理,而是把关键指标摊在桌上让人自己看。

维度Tailwind CSSBootstrapBulmaPico CSSUnoCSS
核心范式原子类组件类组件类(纯 CSS)语义标签默认样式原子类生成引擎
是否含 JS否是(可选引入)否否否
产物体积(压缩后大致量级)取决于扫描结果,通常很小中到大,按需裁剪后可接受中等很小极小
上手成本中(要记类名体系)低低极低中偏高(要懂配置)
定制自由度极高中(Sass 变量 + CSS 变量)中低极高
设计稿还原难度低中(容易被预设风格带偏)中高(默认审美固定)低
典型项目中后台、产品站、组件库快速交付、外包、内部系统多框架混用项目文档、原型、小工具追求极致构建性能的项目

这张表里我最想强调的是"设计稿还原难度"这一行。很多人选 Bootstrap 是因为"组件全",但真正做起来会发现,把设计稿里那套圆角、阴影、间距往 Bootstrap 上套,往往比从零写还费劲——它的默认视觉语言太强了,你会不断地在"改它"和"绕过它"之间消耗时间。


2. Tailwind CSS:原子化在 2024 年成为默认答案的真实原因

先说结论:Tailwind 赢的不是"写起来快",而是"改起来不心疼"和"删起来不留垃圾"。这两点只有真正维护过一个跑了两年、换过三任开发的项目的人才能体会。

传统写法里,你给一个卡片起名.card-header-title,三个月后需求变了,这个类可能只有一处用到,但没人敢删——万一别的地方也用呢?于是 CSS 文件只增不减,两年后变成一座没人敢碰的垃圾山。原子化的做法把"这段样式属于谁"这个问题彻底消解了:样式跟着标签走,删掉标签样式就没了。

2.1 它解决的从来不是"手速问题"

刚接触 Tailwind 的人第一反应通常是"这不就是把 CSS 写成一行吗,手写不是更自由"。这个理解偏了。它真正的收益点有三个,都在工程侧。

第一是作用域天然隔离。原子类本身就是唯一的,不存在两个开发者起了同名 class 导致样式互相覆盖的情况。这在多人协作、多人同时改同一个页面的项目里,能消灭掉一大类低级冲突。

第二是设计约束内建。p-4、p-8这些数值来自一套统一的间距刻度,团队新人很难写出padding: 13px这种奇数值。一套设计系统能不能落地,靠的从来不是规范文档,而是"不规范就写不出来"的工具约束。

第三是未使用样式自动消失。构建时按内容扫描生成,页面里没出现的类不会进产物。这一点在长期项目里的复利非常明显——CSS 体积不会随着迭代线性膨胀。

2.2 配置文件才是整个工程的重心

2024 年绝大多数项目用的还是 v3.4 这条线,配置方式是基于 JS 的tailwind.config.js。这个文件承担的角色远比"加几个颜色"重要,它其实是设计 token 的唯一来源。

/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './index.html', './src/**/*.{vue,js,ts,jsx,tsx,mdx}' ], theme: { extend: { colors: { brand: { 50: '#eff6ff', 500: '#2563eb', 600: '#1d4ed8' } }, spacing: { 18: '4.5rem' }, borderRadius: { card: '10px' } } }, plugins: [] }

这里有个我踩过的坑:theme和theme.extend的区别。直接写theme.colors是整体替换,会把默认的red、blue、gray全套色板干掉,改完之后发现text-gray-600全失效了,排查半天才想起来是自己覆盖了。要追加必须放extend里面。这个错误几乎每个新手都会犯一次,而且报错信息是"样式没了",不是"配置错了",非常误导。

另一个实际经验:把品牌色、间距、圆角这类 token 全部在配置里定义好,组件里禁止出现任意值写法(w-[137px]这种)。任意值不是不能用,但要用在"确实需要微调"的地方,一旦变成日常操作,设计系统就名存实亡了。

2.3 原子化最容易翻车的三个地方

第一个坑:动态拼接类名。这是最经典的事故。

// 这样写,构建后样式大概率不生效 const cls = `bg-${color}-500`

原因很直白:生成器是按字符串扫描的,它看到的是bg-${color}-500这段字面量,不会去做表达式求值。解决办法有两个,一是写完整类名映射表:

const colorMap = { red: 'bg-red-500', green: 'bg-green-500', blue: 'bg-blue-500' } const cls = colorMap[color]

二是在配置里用safelist显式列出可能被拼出来的类。我一般推荐第一种,因为它对构建器最友好,也不会因为 safelist 写漏而线上缺样式。

第二个坑:@apply用上瘾。@apply允许你在 CSS 里复用一组原子类,看起来像在写传统 CSS。问题在于,抽出去之后这段样式就不再参与"按使用情况生成"的逻辑了,容易变成新的黑盒。更麻烦的是在<style scoped>里大量@apply,会让人分不清哪些样式是原子类、哪些是自定义类,改需求时要同时看两个地方。我的做法是只在极少数跨组件复用的基类上用@apply,其余一律直接写在模板里。

第三个坑:和 UI 组件库打架。Tailwind 的 preflight(基础重置)会把button、input、h1等元素的默认样式抹平,这会让一些组件库的样式错位。典型表现是按钮高度不对、标题字号变小。处理方式有两种:直接关掉 preflight,或者用 CSS Layer 把基础重置的优先级压低。

module.exports = { corePlugins: { preflight: false } }

关掉之后记得自己补一份最小重置,否则不同浏览器的默认间距会给你惊喜。

还有一个不那么显眼但很值得做的事:引入tailwind-merge(运行时解决类名冲突)加eslint-plugin-tailwindcss(构建前强制类名顺序)。前者解决的是"父组件传了px-4、子组件写了px-6,谁生效取决于生成顺序而不是 class 书写顺序"这个反直觉行为;后者让团队里的类名排列保持一致,代码评审时不用再为顺序吵架。


3. Bootstrap 5:老牌选手剩下的地盘,比很多人想的更稳固

网上唱衰 Bootstrap 的声音持续了好几年,但如果你去翻一些外包项目、政府类信息系统、企业内部审批流,会发现它的出场率高得惊人。原因不复杂:它的"默认好看"水平,恰好是大量项目的验收标准线。客户要的不是设计感,是"看起来正规、按钮能点、表格能翻页"。

Bootstrap 5.3 这条线做的几件事很关键:彻底移除 jQuery 依赖、把主题色迁移到 CSS 自定义属性、原生支持[data-bs-theme="dark"]的暗色切换。这意味着过去那套"必须用 Sass 重新编译才能换主色"的流程,在很多场景下可以退化成运行时的几行 CSS。

3.1 什么样的项目用它反而更省钱

我的判断标准很简单,满足两条以上就该认真考虑 Bootstrap:

  • 交付周期紧,且页面以"表单 + 表格 + 弹窗"为主
  • 团队里前端经验参差,有后端兼做页面
  • 验收方对视觉没有强诉求,但对"组件齐全"有明确期待
  • 项目预期寿命不长,或者后续维护人不确定

反过来说,什么项目不该用?设计稿精细到像素、有独立设计团队、需要长期演进品牌视觉的产品站。这类项目用 Bootstrap,你 60% 的时间在拆它的默认样式,性价比极低。

这里还有个常被忽略的成本:Bootstrap 的 JS 组件用的是自己那套 data 属性 API,和 Vue/React 的响应式模型是两套逻辑。在框架项目里直接用它原生的 modal、dropdown,很容易出现"组件卸载了但 backdrop 还在"、"data 状态和组件状态不同步"的问题。比较稳的做法是只用它的 CSS,JS 部分用生态里对应的封装版组件,或者干脆自己写交互。

3.2 Sass 变量定制的正确姿势与常见误操作

Bootstrap 的源码大量使用!default标记,意思是"如果外部没定义,我才用默认值"。这决定了定制顺序:你的变量必须在引入源码之前赋值。

// 正确顺序:先改变量,再引入 $primary: #1677ff; $border-radius: .375rem; $enable-shadows: false; $font-size-base: .9375rem; @import "bootstrap/scss/functions"; @import "bootstrap/scss/variables"; @import "bootstrap/scss/mixins"; @import "bootstrap/scss/bootstrap";
// 错误顺序:引入之后再改,不会生效 @import "bootstrap/scss/bootstrap"; $primary: #1677ff; // 无效

第二个常见误操作是改完$primary就以为完事了,结果发现.btn-primary的 hover、active 颜色还是原来那个蓝。因为 Bootstrap 用$primary派生出$primary-hover、$theme-colors等一整套变量,只在引入前赋一个值,派生计算是对的,但如果你在中间插入了额外的 import 打乱了顺序,派生就会用旧值。稳妥做法是把所有变量集中在一个_custom.scss里,然后在入口只有一个 import。

第三个坑是关于 5.3 的 CSS 变量:它可以运行时改--bs-primary,但不会自动重算那些用 Sass 编译期算出来的衍生色。所以想要完整的自定义主题色,仍然是 Sass 编译这条路更靠谱;纯 CSS 变量方案适合做"轻量换色",比如把主色调整几个色阶。

3.3 和原子化混用时的边界约定

现实里很常见的场景是:老项目是 Bootstrap,新页面想用 Tailwind。两边混着写不违法,但必须提前定边界,否则半年后没人说得清某个样式从哪来。

我一般按"页面级隔离"来定:老页面继续用 Bootstrap 的栅格和组件类,新页面整页使用原子类,中间用一个容器层切断。具体做法是给新页面根节点关掉 Bootstrap 的排版影响,或者在 Tailwind 配置里加前缀。

module.exports = { prefix: 'tw-', content: ['./src/**/*.{vue,tsx}'] }

prefix会让所有原子类变成tw-flex、tw-p-4这种形式。好处是一眼能看出来源,坏处是写起来啰嗦。要不要加前缀,取决于混用范围:只是偶尔几个页面,直接混写、靠代码评审控制就够了;如果是长期共存的两套体系,前缀基本是必须的。


4. Bulma 与 Pico:两种"轻"的不同解法

把这两个放一起讲,是因为它们经常被同一类需求拿出来比较:"我就想要一套现成的样式,不想引框架、不想配构建、不想学新语法"。但它们的解法完全相反,适用的项目也完全相反。

4.1 Bulma 的无 JS 组件哲学

Bulma 的核心决策是"只做样式,不做交互"。它的 navbar、modal、dropdown、tab 全都只提供静态的类名组合和is-active这样的状态类,什么时候加、怎么加,交给你。

这个设计在 2024 年的技术环境下反而合理:项目大概率已经用了某个组件化框架,交互逻辑本来就该由框架的状态管理来做。如果引入一个自带 JS 的样式框架,你等于在一个页面里维护两套状态机。我见过一个项目里 Bootstrap 的 modal 和 Vue 的弹窗组件同时存在,最后出现"打开一个弹窗,页面上多了三个 backdrop"的盛况。

Bulma 的另一个特点是它的类名设计非常"语义直白":is-primary、is-large、has-text-centered。这种写法对非前端背景的开发者很友好,看一遍就大概能猜到效果。它拖了很多年才走到 1.0,主要变化是把主题变量往 CSS 自定义属性上迁移,改主题不再必须依赖 Sass 重编译。

不过它的问题也很实在:组件覆盖面比 Bootstrap 窄。一些复合控件(比如复杂的日期选择、级联选择)它压根不提供,你还是得自己找或自己写。所以它更适合"页面结构清晰、交互简单"的项目:官网、产品介绍页、轻量后台。

实际使用建议:如果你决定用 Bulma,先在项目里定一个约定——所有涉及状态切换的类,统一由一个状态映射函数产出,不要在各个组件里零散地写classList.add('is-active')。这样后续要调整状态类命名时只改一处。

4.2 Pico 的语义化默认样式是怎么省事的

Pico CSS 走的是另一条路:classless,也就是你几乎不写 class。它的规则是"把 HTML 写对,我来负责好看"。

<article> <header> <h2>标题</h2> </header> <p>正文段落。</p> <footer> <button>确认</button> </footer> </article>

就这么几行,页面出来会是带内边距、圆角、分层阴影的卡片。它的价值在于"从 0 到 60 分"这段路几乎是零成本:不用配置、不用构建、引一个 CDN 链接就完事。

它的边界也很清晰。第一,默认审美是固定的,你能调的就是那几个 CSS 变量(主色、字号、间距、圆角),想做精细设计就得开始覆盖它的样式,一旦开始覆盖,classless 的优势就没了。第二,组件种类少,没有表格分页、没有多级菜单、没有复杂表单控件。第三,它假设你的 HTML 结构规范,如果你手上是一堆<div>套<div>的旧代码,它的默认样式基本不起作用。

我的实际使用经验是:它特别适合内部文档、技术说明页、配置后台的小工具页,以及做原型快速验证。不适合任何需要"按设计稿还原"的场景。

4.3 轻量框架在真实项目里的天花板

这两个框架的讨论价值,其实在于帮你判断"轻"的边界在哪。我总结出一个粗糙但好用的观察:

项目阶段轻量框架是否够用说明
原型 / Demo完全够用目标是能跑通流程,审美不重要
内部工具(< 20 页面)大概率够用用户是自己人,容忍度高
面向客户的产品前 3 个月够用迭代快,样式不是瓶颈
产品进入第 2 年开始不够设计要求变细,组件需求变多
多人协作的中大型系统不够缺少统一的状态类和组件约定

说到底,轻量框架省下的是"搭建期"的成本,透支的是"演进期"的灵活性。如果你的项目注定要做两年以上,选轻量框架前先想清楚:现在省下的两周,未来要用多少周来还。


5. UnoCSS:把框架从"一套类名"变成"一台生成引擎"

第一次看 UnoCSS 的文档会有点懵:它不告诉你有哪些类可以用,而是告诉你"你可以让哪些类存在"。这个思路差别很大,值得单独拆开讲。

5.1 引擎化和插件化的差别到底在哪

Tailwind 的模式是:给你一套完整的、有明确边界的类名集合,加扩展靠插件,但核心规则是固定的。UnoCSS 的模式是:核心只做一个高速的"规则匹配 + 按需生成"引擎,具体有哪些规则,由你挂载的 preset 决定。

挂presetUno就得到接近 Tailwind 的语法,挂presetWind就是严格的 Tailwind 兼容,挂presetAttributify就能写<div flex items-center>这种不带前缀的属性写法,挂presetIcons就能把图标当类名用(i-carbon-user),按需打包 SVG。这些能力在 Tailwind 生态里需要多个插件拼起来,在 UnoCSS 里是配置层面的排列组合。

性能也是它主打的点。它用了一套更精简的匹配流程,在大型项目里冷启动和热更新都比走 PostCSS 的方案更快,特别在 Vite 这种依赖原生 ESM 的构建环境里体感明显。

5.2 一份能直接用的配置

// uno.config.ts import { defineConfig, presetUno, presetAttributify, presetIcons, transformerDirectives, transformerVariantGroup } from 'unocss' export default defineConfig({ presets: [ presetUno(), presetAttributify(), presetIcons({ scale: 1.2, warn: true }) ], transformers: [ transformerDirectives(), transformerVariantGroup() ], shortcuts: { 'btn': 'px-4 py-2 rounded inline-flex items-center justify-center cursor-pointer select-none', 'btn-primary': 'btn bg-blue-600 text-white hover:bg-blue-700', 'card': 'bg-white rounded-lg shadow-sm p-4' }, theme: { colors: { brand: { 500: '#2563eb' } } } })

shortcuts这个能力值得重点说。它相当于"组合类",但生成的是原子类而不是手写 CSS,所以依然享受按需生成。上面这段里btn-primary复用了btn,改一次基础间距,所有按钮跟着变。它解决的是原子化最被诟病的"类名太长"问题,同时不引入@apply那种脱离管控的风险。

transformerVariantGroup也很有用,它让你能写hover:(bg-blue-700 text-white)这种分组语法,长串类名的可读性改善明显。

5.3 扫描失效:UnoCSS 最容易让人怀疑人生的地方

按需生成的前提是"能扫到"。扫不到就等于样式不存在,而且不报错,界面上就是没效果。

最常见的三种扫不到:

第一种,类名写在被排除的目录里。默认扫描范围有限,如果你的模板放在server/views或者legacy/这种非常规路径,需要显式加进去。

export default defineConfig({ content: { pipeline: { include: [ /\.(vue|svelte|[jt]sx|mdx?|html)($|\?)/, 'server/views/**/*.html' ] } } })

第二种,动态拼接。这个和 Tailwind 是同一个坑,但 UnoCSS 里更隐蔽,因为它的规则匹配更宽泛,有时候你拼接的类恰好能被某个模糊规则匹配上,于是"有的颜色生效、有的不生效",排查起来极其费劲。解决办法同样是建映射表,或者用safelist。

第三种,在第三方组件内部传类名。属性化模式(attributify)在遇到会把属性透传到 DOM 之外的组件时容易出问题,尤其是那些把 props 重新组合再渲染的封装组件。遇到这种情况,我会在这个组件上退回普通类名写法,不要为了统一而硬扛。

另外一个实操提醒:presetIcons需要额外装图标集依赖,不装的话类名会被当成无效规则静默丢弃。我第一次用的时候对着一个空白图标位置看了十分钟,最后发现是忘了加@iconify-json/xxx。


6. 用场景反推选型:四类项目的决策路径

讲完五个框架,回到最实际的问题:如果你手上正好有个项目要开,怎么选。我习惯不看"框架能力",看"项目约束",因为能力都能补,约束补不了。

6.1 后台管理系统

这类项目的特征很明确:表单多、表格多、弹窗多、页面数量大、迭代频繁、视觉要求中等。

如果团队 3 人以上、项目预期超过一年,我倾向Tailwind + 一个无样式组件库的组合。无样式组件库负责交互和无障碍,样式全交给原子类。这样既拿到了组件能力,又不会被预设视觉绑住。

如果团队只有 1 到 2 人、要两个月上线,Bootstrap更划算。你要的表格、分页、模态、表单校验提示,它全都有现成的,直接抄文档就能用。

如果项目是给内部几十个人用的工具,页面不到 20 个,Pico都能撑住,省下来的配置时间够你多写两个功能。

6.2 内容型站点与官网

营销页、产品介绍页、博客这类,特征是:页面数量中等、视觉要求高、交互简单、可能有 SEO 和首屏性能要求。

这类我几乎不犹豫选原子化。设计稿往往是一次性的、高度定制的,用组件框架反而要不停覆盖默认样式。构建产物小、首屏快,对 SEO 有实际帮助。

唯一要注意的是内容由非技术人员维护的场景。如果文章正文是运营在后台富文本编辑器里写的,编辑器输出的 HTML 不带你的原子类,那正文区域的排版还是得用一段独立的手写 CSS 兜底。这个细节经常被漏掉,上线后发现"后台写的文章在页面上完全没有样式"。

6.3 组件库 / 多端复用项目

如果要做的是一套自己的组件库,或者要同时输出 Web、小程序、桌面端,选型逻辑完全不同:样式的可移植性优先于开发效率。

这时候纯 CSS 的框架(Bulma 这类)或者引擎化的方案(UnoCSS)更合适。前者不绑定任何运行时,样式文件可以直接搬到别的技术栈;后者可以把同一套规则通过不同 preset 输出到不同目标,也能生成纯 CSS 给非前端环境用。

反过来,重度依赖 JS 的框架在这类项目里是负担——你没法把 Bootstrap 的 modal 逻辑搬到小程序里。

6.4 一张按约束查的决策表

你的主要约束首选备选不建议
交付时间极紧,人手少BootstrapPico自建体系
设计稿精细,视觉是卖点TailwindUnoCSSBootstrap
团队前端经验参差BootstrapTailwind(配好约束)UnoCSS
项目要跑三年以上TailwindUnoCSSPico、Bulma
需要多端复用样式UnoCSSBulma含 JS 的框架
只是做原型验证PicoBulma任何需要配置的
要兼容老浏览器Bootstrap 4 线手写 CSS原子化最新版

最后一行值得多说一句。原子化框架的新版本普遍依赖现代 CSS 特性,如果项目明确要求兼容很老的浏览器环境,选型前一定要确认构建目标和降级方案,不要等上线前才发现某些写法需要额外配置。


7. 迁移与共存:老项目换框架的现实做法

技术选型文章最容易缺的一块,是"我已经有一个项目了,怎么办"。整站重写听起来痛快,但现实里 90% 的情况是渐进替换。

7.1 渐进式共存的三种隔离策略

策略一:前缀隔离。给新体系的所有类加统一前缀(tw-之类),和老类名物理上不重叠。优点是实现简单、边界清晰;缺点是写法变长,且无法复用生态里现成的配置片段。

策略二:作用域隔离。用 CSS Layer 或者容器作用域把两套体系的优先级分开。

@layer legacy, base, components, utilities;

把老样式放进legacy层,新样式放在后面几层。层与层之间的优先级关系是固定的,不参与选择器权重计算,所以新样式天然压过老样式,不需要靠!important硬怼。这个方案的好处是不用改类名,代价是需要构建工具支持,且要梳理清楚哪些老样式该进哪一层。

策略三:页面级隔离。老页面一套、新页面一套,互不相干,靠路由切换。最简单粗暴,也最不容易出问题,适合"整体重构、分批上线"的节奏。缺点是两套样式都要加载,短期体积会变大。

我实际项目里用得最多的是策略二和策略三的组合:路由级分新旧,新旧内部再用 Layer 控制优先级。

7.2 样式优先级冲突的完整排查链路

迁移期最典型的症状是:"我明明写了这个样式,页面上就是不生效。" 遇到这种情况我按固定顺序排查,基本能在十分钟内定位。

第一步,看 DevTools 里的 computed 来源。打开元素面板,看右侧样式列表,被划掉的那条就是被覆盖的,鼠标悬停能看到覆盖它的规则来自哪个文件。这一步能过滤掉 80% 的猜测。

第二步,确认 CSS 的加载顺序。这是最隐蔽的一类问题。构建工具做代码分割后,CSS 的加载顺序不再固定,同一个组件在不同路由下可能拿到不同的样式顺序,导致"首页正常、详情页错乱"这种诡异现象。判断方法是刷新页面看样式列表里两个规则文件的先后,如果顺序会变,那就是它。

第三步,检查是否在 Shadow DOM 边界内。Web Components 内部的样式和外部是隔离的,外部写的选择器进不去(除非用 CSS 自定义属性或 part)。迁移时如果发现有组件怎么改都没反应,先确认它是不是 shadow 封装。

第四步,检查 specificity 而不是加!important。很多人第一反应是加!important,加完之后确实生效了,但下一个开发者要覆盖它只能再叠一个!important,最后整份样式全是感叹号。正确的做法是把选择器提一层,比如从.btn提到.page .btn,或者用 Layer 调整层级。

第五步,确认构建产物里真的有这条规则。原子化方案下,样式没生成和样式被覆盖,在页面上的表现是一样的,但原因完全不同。去产物 CSS 里搜一下类名,能立刻区分这两类问题。

7.3 迁移过程中最值得投入的两件事

第一件是建立视觉回归基线。迁移前把核心页面截图存档,迁移后用同样的视口尺寸再截一次做对比。人工看是看不出来的,一个 2px 的间距变化在页面上几乎不可见,但堆叠起来就是"这个页面感觉不对但说不上哪不对"。这件事前期投入几个小时,能省掉后面无数次返工。

第二件是写一份类名映射表。老类名到新写法的对应关系,哪怕只是一个 Markdown 表格,价值也极高。它让迁移工作可以拆给多个人并行做,也让后来的人知道某个地方为什么这么写。我在一个项目里没做这件事,半年后接手的人问我"为什么这个按钮用两套类名",我自己都想不起来原因。

还有一个细节:迁移期间不要同时升级构建工具。样式问题已经够难排查了,如果构建链路也在变,出问题时你没法判断是框架的问题还是构建的问题。分两步走,先迁样式,稳定之后再动工具链。


聊到这里,我个人最实在的一条体会是:这五个框架里,真正决定项目好不好维护的,从来不是选了哪一个,而是有没有在开工第一天就把约束定下来——间距刻度是什么、颜色从哪来、什么情况下允许写任意值、新老样式怎么划界。这些东西写在文档里没人看,但做成配置和检查规则之后,它会替你把关。我见过用 Bootstrap 写得非常干净的项目,也见过用原子化写得一团乱麻的项目,差别全在约束上,不在框架上。

如果只让我给一个建议:先用两小时把项目未来一年的页面数量和团队人数估出来,再去对照上面那张约束表。估错了框架还能换,估错了约束,换什么都一样。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进