因为Vite将CSS视为资源走load+内置插件链,而原子化CSS需在JS/TS解析阶段扫描类名,必须用transform钩子拦截源码并AST提取类名,注入虚拟模块实现按需生成与HMR。

为什么不能直接在 vite.config.ts 里写 CSS 处理逻辑
因为 Vite 的插件生命周期中,transform 钩子对 .css 文件默认不触发(Vite 把 CSS 当作资源处理,走的是 load + 内置 CSS 插件链),而原子化 CSS 需要在 JS/TS 模块解析阶段就扫描类名、生成对应规则——必须拦截源码字符串,不能等 CSS 文件落地。
常见错误是试图在 load 钩子里读取 .css 文件内容再处理,结果发现根本收不到请求,或者收到的是已编译的产物 CSS,失去原子化“按需生成”的意义。
- 正确入口是
transform钩子,且include要覆盖**/*.{ts,tsx,js,jsx,vue,svelte} - 必须用正则或 AST 安全提取类名(比如
class="text-red-500 p-4"或clsx("m-2", condition && "hidden")),不能靠字符串split(" ")粗暴切分 - 生成的 CSS 规则要注入到一个虚拟模块(如
virtual:windi.css),再由 Vite 自动 resolve 和走 CSS 处理流程
如何安全提取 JSX/TSX 中的动态类名
用正则匹配 className、class、tw、clsx 等调用容易漏掉嵌套条件或模板字符串,真实项目里常见 className={`${base} ${isActive ? 'active' : ''}`} 这种写法。
推荐用 @babel/parser + @babel/traverse 在 transform 阶段做轻量 AST 分析,只关注 JSXAttribute 和 CallExpression 节点,不走完整构建流程,开销可控。
立即学习“前端免费学习笔记(深入)”;
- 对
clsx/cva/tw等函数调用,递归遍历参数,提取字面量字符串和带?:的三元表达式分支中的字面量 - 跳过含变量、函数调用、复杂对象展开的参数(如
clsx(styles.button, { 'is-loading': loading })不解析对象键) - 把提取出的类名统一存入全局 Set,避免重复生成相同规则
怎么让生成的 CSS 实时热更新而不刷新页面
Vite 的 HMR 对虚拟模块支持良好,但关键在于:你得主动告诉 Vite 哪些模块依赖这个虚拟 CSS,否则改了类名,CSS 不会重编译。
做法是在每次 transform 后,调用 this.invalidate('virtual:atomic.css'),并确保所有源文件都 import 了该虚拟模块(哪怕只是 import 'virtual:atomic.css' 一行)。
- 不要用
fs.writeFileSync写真实 CSS 文件——这绕过了 Vite 的模块图,HMR 失效 - 虚拟模块的
load钩子必须返回完整的 CSS 字符串,且每次都要重新生成(不能缓存旧内容) - 如果用了 PostCSS(比如加 autoprefixer),确保在虚拟模块的
load返回前处理完毕,而不是丢给后续插件
为什么开发时 CSS 规则顺序会错乱
多个组件同时 import 虚拟 CSS,Vite 默认按 import 顺序拼接,但原子化规则有优先级(比如 !important、hover: 伪类需要后置),纯字符串拼接无法控制。
解决办法是把所有生成的规则按预设层级分类(base / components / utilities / variants),再用 PostCSS 插件做一次归一化排序,最后合并输出。
- utility 类(如
text-center)必须排在 component 类(如btn)之后,否则会被覆盖 -
hover:、group-hover:等变体规则必须放在最末,且保证同一选择器内伪类顺序一致 - 避免在
transform钩子里直接拼 CSS 字符串,改用结构化数据(如{ selector: '.text-red-500', declarations: [...] })便于后期排序


















