摘要 :本文深度复盘了 Coco AI 项目在引入 shadcn/ui 组件库的同时,激进升级至 Tailwind CSS 4.0 的技术细节。重点剖析了在 Vite + Tsup (Esbuild) 双构建工具链下的兼容性方案,以及如何处理
tailwind.config.js与 CSS-first 配置模式的冲突,为维护大型遗留项目的开发者提供一份"硬核"避坑指南。
前言:为什么要自找麻烦?
在 Coco AI 的开发过程中,我们面临着大多数成长期项目都会遇到的痛点:
- UI 碎片化:早期的手写 CSS 与后期的 Tailwind Utility Class 混杂,维护成本极高。
- 重复造轮子:为了一个带键盘导航的 Dropdown,我们可能写了 500 行代码,且 Bug 频出。
引入 shadcn/ui 是为了解决组件复用问题,而升级 Tailwind CSS v4.0 则是为了追求极致的构建性能(Rust 引擎)。当这两者在这个拥有大量遗留代码的项目中相遇时,一场"构建工程化的风暴"不可避免。
本文不谈虚的,直接上干货。
难点一:Vite 与 Tsup 的"双轨制"构建困局
Coco AI 不仅是一个 Web 应用,还包含一个对外提供的 SDK。这就导致我们有两套构建流程:
- Web App : 使用 Vite (Rollup)。
- Web SDK : 使用 Tsup (Esbuild)。
Tailwind v4 推荐使用 @tailwindcss/vite 插件,这在 Web App 中运行良好。但在 SDK 构建中,Esbuild 并不支持该插件。
解决方案:混合编译策略
我们被迫采用了一套"混合"方案:Web 端享受 v4 的插件红利,SDK 端则回退到 PostCSS 处理。
1. Web 端 (Vite)
一切从简,使用官方插件。
typescript
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
// 这里的 tailwindcss() 会自动扫描文件,性能极快
plugins: [tailwindcss() as any, react()],
});
2. SDK 端 (Tsup/PostCSS)
这是最坑的地方。Tsup 基于 Esbuild,而 Esbuild 默认无法解析 v4 的 @import "tailwindcss";。我们需要手动配置 PostCSS 管道。
首先,配置 postcss.config.js,显式使用 v4 的 PostCSS 插件:
javascript
// postcss.config.js
export default {
plugins: {
// ⚠️ 注意:Tailwind v4 的 PostCSS 插件包名变了
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}
然后,在 tsup.config.ts 中施展"魔法":
typescript
// tsup.config.ts
export default defineConfig({
esbuildOptions(options) {
// 🔥 关键黑魔法:启用 'style' 条件,让 esbuild 能找到 tailwindcss 的入口
(options as any).conditions = ["style", "browser", "module", "default"];
},
async onSuccess() {
// 构建后手动运行 PostCSS,处理 CSS 文件中的 @import "tailwindcss"
// ...代码略,见源码...
}
});
难点二:JS 配置与 CSS 配置的"博弈"
Tailwind v4 推崇 CSS-first ,即把配置都写在 CSS 的 @theme 块中。但 shadcn/ui 强依赖 tailwindcss-animate 插件,且我们有大量复杂的自定义动画(如打字机效果、震动效果)写在 tailwind.config.js 中。
如果完全迁移到 CSS,工作量巨大且易出错。
解决方案:JS 与 CSS 共存
我们保留了 tailwind.config.js,主要用于存放插件 和复杂动画 ,而将颜色变量迁移到 CSS 中。
保留的 tailwind.config.js (部分):
javascript
import animate from "tailwindcss-animate";
export default {
// v4 会自动检测并合并这个配置
theme: {
extend: {
// 复杂的 Keyframes 还是写在这里比较清晰
animation: {
typing: "typing 1.5s ease-in-out infinite",
shake: "shake 0.5s ease-in-out",
},
keyframes: {
typing: {
"0%": { opacity: "0.3" },
"50%": { opacity: "1" },
"100%": { opacity: "0.3" },
},
// ...
},
// 映射 border-radius 到 CSS 变量,适配 shadcn
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [animate], // shadcn 必需的插件
};
新的 src/main.css (v4 风格):
css
@import "tailwindcss";
/* ⚠️ 必坑点:显式指定扫描源,否则可能漏掉 HTML 或特定目录 */
@source "../index.html";
@source "./**/*.{ts,tsx}";
@theme {
/* 在 CSS 中通过变量映射颜色,不仅支持 shadcn,还能兼容旧代码 */
--color-background: var(--background);
--color-primary: var(--primary);
/* ... */
}
难点三:颜色空间与暗色模式的"大一统"
Coco AI 的旧代码使用 RGB 值(如 rgb(149, 5, 153)),而 shadcn 使用 HSL(如 222.2 84% 4.9%),Tailwind v4 默认又倾向 OKLCH。
解决方案:变量映射层
我们在 main.css 中建立了一个"中间层",让新老变量和谐共存。
css
:root {
/* === Shadcn 系统 (HSL) === */
--primary: 221.2 83.2% 53.3%;
/* === Coco Legacy 系统 (RGB) === */
/* 即使是旧变量,也可以根据需要调整,或者直接硬编码保留 */
--coco-primary-color: rgb(149, 5, 153);
}
/* ⚠️ v4 暗色模式新语法:废弃了 darkMode: 'class' */
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
.dark.coco-container,
[data-theme="dark"] {
/* 重新定义 HSL 值实现暗色模式 */
--background: 222.2 84% 4.9%;
/* 同时覆盖旧系统的变量 */
--coco-primary-color: rgb(149, 5, 153);
}
难点四:Web SDK 的 CSS 变量兼容性黑科技
在开发 Web SDK 时,我们遇到一个隐蔽的问题:CSS 变量的初始值丢失。
Tailwind v4 会生成大量的 CSS Houdini @property 规则来定义变量的类型和初始值:
css
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
这在现代浏览器中运行完美。但由于我们的 SDK 会被嵌入到各种宿主环境中,部分环境可能不支持 @property,导致变量因为没有显式的赋值而失效(initial-value 被忽略)。
解决方案:构建后脚本补全 (Post-build Script)
为了保证"即插即用"的稳定性,我们编写了一个专门的构建后处理脚本 scripts/buildWebAfter.ts。
它的作用是:扫描生成的 CSS,提取所有 @property 的 initial-value,并将它们显式注入到 .coco-container 作用域中。
typescript
// scripts/buildWebAfter.ts (精简版)
const extractCssVars = () => {
const cssContent = readFileSync(filePath, "utf-8");
const vars: Record<string, string> = {};
// 正则提取所有 @property 的 initial-value
const propertyBlockRegex = /@property\s+(--[\w-]+)\s*\{([\s\S]*?)\}/g;
while ((match = propertyBlockRegex.exec(cssContent))) {
const initialValueMatch = /initial-value\s*:\s*([^;]+);/.exec(match[2]);
if (initialValueMatch) {
vars[match[1]] = initialValueMatch[1].trim();
}
}
// 生成标准的 CSS 变量赋值块
const cssVarsBlock =
`.coco-container {\n` +
Object.entries(vars)
.map(([k, v]) => ` ${k}: ${v};`) // 显式赋值:--var: value;
.join("\n") +
`\n}\n`;
writeFileSync(filePath, `${cssContent}\n${cssVarsBlock}`, "utf-8");
};
效果 :即使浏览器不支持 @property,变量也能通过标准的 CSS 级联机制获得正确的初始值,确保 SDK 在任何环境下样式都不崩坏。
避坑清单 (Checklist)
在迁移过程中,我们踩了无数坑,以下是血泪总结:
-
样式莫名丢失?
- 原因:Tailwind v4 的自动扫描可能没覆盖到你的文件结构。
- 解法 :使用
@source指令显式添加路径,如@source "./**/*.{ts,tsx}";。
-
VS Code 满屏报错?
- 原因 :VS Code 的 Tailwind 插件版本过低,不认识
@theme、@source等新指令。 - 解法:升级插件到最新版,并确保设置中关联了正确的文件类型。
- 原因 :VS Code 的 Tailwind 插件版本过低,不认识
-
构建时报错
Cannot find module?- 原因 :
postcss.config.js中引用了不存在的插件。 - 解法 :确认安装了
@tailwindcss/postcss并在配置中正确引用(注意包名变化)。
- 原因 :
-
动画不生效?
- 原因 :
tailwind.config.js未被 Vite 插件读取。 - 解法 :在使用
@tailwindcss/vite时,它通常会自动检测根目录下的配置文件。如果位置特殊,需手动指定。
- 原因 :
小结
技术债是还不完的,但每一次还债都是一次成长的机会。
通过这次适配,Coco AI 不仅拥有了更现代化的 UI 架构,也为未来的跨平台(Web/Desktop/Mobile)统一体验打下了基础。特别是 Tailwind CSS v4.0 的引入,虽然初期配置略显折腾,但其带来的构建速度提升和开发体验优化,绝对是"真香"定律的又一次验证。
如果你也想体验一下这个"整容"后的全能生产力工具,欢迎来我们的 GitHub 看看:
- GitHub : github.com/infinilabs/...
- Website : coco.rs/en