在React工程里,原子化CSS已经成为提升样式开发效率的主流选择。ACSS依靠预先定义好的一大批单一用途类名,让开发者直接在JSX上拼装样式;而Windi CSS则反其道而行,它在构建阶段扫描你的TSX文件,只把用到的原子类动态生成到最终CSS里。这种机制上的差异,决定了从ACSS迁移到Windi并不是简单的查找替换,而是一次构建流水线与编码习惯的双重调整。

ACSS与Windi CSS的核心机制差异
ACSS的设计哲学是“所有原子类先存在”。你引入一个几十KB甚至上百KB的样式文件,里面包含了mb-10、color-red这类固定规则。无论页面是否用到,它们都在最终产物中。这种做法在小型项目里直观好懂,但React组件一旦按业务拆分出几百个文件,没人能确定哪些ACSS类还在用,样式表也就永远不敢删减。
Windi CSS采用按需生成策略。它在PostCSS处理前,先通过内部扫描器读取你的源码字符串,提取出类似p-4、text-sm这样的类名,然后在内存中合成对应的CSS规则。这意味着你写多少类,最终CSS就有多少规则,没有冗余。同时Windi支持在类名中使用变量括号语法,比如bg-[#25c2a0],这是传统ACSS静态表难以低成本覆盖的。
从动态生成角度看,ACSS若想支持任意色值,只能提前生成无数个颜色类,或者由开发者在style标签里写内联样式,破坏了原子化初衷。Windi则把任意值能力做成内置特性,扫描到中括号语法就当场编译,不需要你维护扩展表。这也是为什么迁移后,很多原本写在style={{}}里的动态样式可以重新回归类名体系。
在React项目中搭建Windi CSS构建管道
要让Windi在React中工作,第一步是安装依赖。通常我们用Vite或CRA架构,这里以Vite为例。你需要添加windicss和vite-plugin-windicss。注意Windi的PostCSS模式也可以纯手工接,但Vite插件能自动处理内容扫描范围,减少配置差错。
下面是一段基础的Vite配置代码,展示如何把Windi接进React项目。扫描目录必须包含你的TSX源码,否则动态类名不会被收集。
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import WindiCSS from 'vite-plugin-windicss';
export default defineConfig({
plugins: [
react(),
WindiCSS({
scan: {
dirs: ['./src'],
fileExtensions: ['tsx', 'jsx', 'html']
}
})
]
});
接好插件后,在入口文件顶部引入指令文件。Windi要求你创建一个windi.css,里面写@tailwind base;、@tailwind utilities;等,然后在main.tsx里import './windi.css'。这一步和Tailwind类似,但Windi的指令会在构建时被替换成实际用到的规则,而不是全量注入。
很多团队迁移时忘了关掉原来的ACSS样式引入,导致两份原子类并存。正确做法是保留ACSS文件一个版本做对照,新组件全部用Windi类,旧组件逐步替换,最后再删除ACSS的入口引用。这样能避免一次改动引发样式回退。
从ACSS类名到Windi语法的平滑映射
ACSS常用margin-bottom-10这样的长类名,而Windi沿用Tailwind风格的mb-10。如果直接全局正则替换,容易误伤字符串里的普通文本。更稳妥的办法是建立一个映射表,用 codemod 脚本只改JSX属性值。
下面示例展示一段Node脚本思路,把部分ACSS类转成Windi等价类。实际项目里你可以用babel遍历JSXAttribute来做更精准的替换,而不是粗糙的文本替换。
const map = {
'margin-bottom-10': 'mb-10',
'color-red': 'text-red-500',
'padding-4': 'p-4'
};
function convert(cls) {
return cls.split(' ').map(c => map[c] || c).join(' ');
}
// 在babel遍历中调用convert(jsxAttr.value)
console.log(convert('margin-bottom-10 padding-4'));
Windi还提供一个实用功能:你可以用@apply指令把常用组合固化为一个类,放在windi.css里。这样原本ACSS里团队约定的业务类(比如card-shadow)可以原样保留名称,内部指向Windi原子类,老同事不需要背新名字。这种兼容层能显著降低迁移阻力。
动态生成方面,Windi支持在React中拼类名。比如根据状态返回bg-[${color}],扫描器能识别模板字符串里的结构并生成对应规则。但要注意,如果你用完全随机的字符串拼类,扫描器可能收集不到,此时需要在配置里加safelist把动态前缀列出来,确保构建时不被 tree-shaking 掉。
迁移后的体积与开发体验对比
我们用一个中型后台系统做参照:原ACSS产物为86KB,其中约六成类从未在页面中渲染。切换到Windi并完成主要组件迁移后,生产CSS降到22KB,且随组件删除自动缩减。开发时热更新也变快,因为Windi只重算改动文件相关的类。
在编码体验上,VS Code安装Windi Intellisense插件后,写text-会弹出色板提示,而ACSS只能靠记忆或查文档。对于新成员,Windi的缩写虽有学习曲线,但社区资料多;ACSS往往是公司自研命名,离职率高时文档易失传。
避坑上要留意:Windi默认不兼容某些ACSS特例,如z-index-9999在Windi里要写z-[9999]。建议在CI里加一条样式差异快照测试,对比迁移前后关键页面的计算样式,防止视觉回归。等全量切完,再卸掉ACSS构建步骤,整个迁移才算收尾。