Chapter 09

与框架集成

React、Vue、Next.js 的最佳集成实践,以及 clsx/cn 动态类名处理工具

1. React + Tailwind:动态 className

在 React 中,样式通过 className prop 传递。Tailwind 工具类是静态字符串,但实际开发中经常需要根据 state/props 动态切换类名。

基础动态类名

// ❌ 错误:不能用字符串拼接生成类名(v4 需要完整类名才能被扫描)
const size = 'lg';
<div className={`text-${size}`}> // ❌ text-lg 可能不会生成

// ✅ 正确:使用完整的类名字符串
const sizeClass = size === 'lg' ? 'text-lg' : 'text-sm';
<div className={sizeClass}>

// ✅ 模板字符串(但变量必须是完整类名)
<button
  className={`px-4 py-2 rounded-lg ${isActive ? 'bg-cyan-500 text-white' : 'bg-gray-100 text-gray-600'}`}
>
⚠️

JIT 扫描限制:Tailwind 通过静态扫描源码找出使用的类名。如果你动态拼接类名(如 `text-${color}`),JIT 无法识别,这些类不会被包含在输出 CSS 中。必须使用完整的类名字符串。

2. clsx / cn 工具函数

clsx 是一个轻量级工具库,专门处理条件类名合并。cn 是 shadcn/ui 推荐的封装,在 clsx 基础上加了 tailwind-merge 去重。

// 安装
npm install clsx tailwind-merge

// utils/cn.ts —— shadcn/ui 推荐的 cn 工具函数
import { clsx, ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
// 使用示例
import { cn } from '@/utils/cn'

// 条件类名(clsx)
cn(
  'px-4 py-2 rounded-lg font-medium',  // 始终有
  isActive && 'bg-cyan-500 text-white',   // 条件有
  !isActive && 'bg-gray-100 text-gray-600', // 另一条件
  size === 'sm' && 'text-sm',
  size === 'lg' && 'text-lg px-6',
)
// 结果示例:'px-4 py-2 rounded-lg font-medium bg-cyan-500 text-white'

// tailwind-merge 解决冲突(同一属性多个值)
cn('px-4 py-2', 'p-6')  // → 'p-6'(p-6 覆盖 px-4 py-2)
cn('text-blue-500', 'text-cyan-500') // → 'text-cyan-500'

在 React 组件中使用 cn

import { cn } from '@/utils/cn'

interface ButtonProps {
  variant?: 'primary' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  className?: string
  children: React.ReactNode
}

export function Button({
  variant = 'primary',
  size = 'md',
  className,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(
        // 基础样式
        'inline-flex items-center justify-center rounded-lg font-medium',
        'transition-all duration-200 focus-visible:outline-none',
        'focus-visible:ring-2 focus-visible:ring-offset-2',
        // variant
        variant === 'primary' && 'bg-cyan-500 text-white hover:bg-cyan-600 focus-visible:ring-cyan-500',
        variant === 'outline' && 'border-2 border-cyan-500 text-cyan-600 hover:bg-cyan-50',
        variant === 'ghost'   && 'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
        // size
        size === 'sm' && 'text-xs px-3 py-1.5',
        size === 'md' && 'text-sm px-4 py-2',
        size === 'lg' && 'text-base px-6 py-3',
        // 允许外部覆盖(tailwind-merge 处理冲突)
        className
      )}
      {...props}
    >
      {children}
    </button>
  )
}

// 使用
<Button variant="primary" size="lg">提交</Button>
<Button variant="outline" className="w-full">取消</Button>

3. Vue 3 + Tailwind

<!-- Vue 3 动态类名绑定 -->
<template>
  <!-- :class 接受对象/数组 -->
  <button
    :class="[
      'px-4 py-2 rounded-lg font-medium transition-colors',
      isActive ? 'bg-cyan-500 text-white' : 'bg-gray-100 text-gray-700',
      size === 'lg' ? 'text-lg px-6 py-3' : 'text-sm',
    ]"
    @click="toggle"
  >
    {{ label }}
  </button>

  <!-- 对象语法 -->
  <div :class="{
    'opacity-50 cursor-not-allowed': disabled,
    'hover:scale-105': !disabled,
    'bg-cyan-500': isPrimary,
    'bg-gray-200': !isPrimary,
  }"></div>
</template>

<script setup>
import { ref } from 'vue'

const isActive = ref(false)
const toggle = () => isActive.value = !isActive.value
</script>

4. Next.js 15 配置

# 创建 Next.js 15 项目(选择 Tailwind CSS)
npx create-next-app@latest my-app
# 选项:✔ TypeScript ✔ ESLint ✔ Tailwind CSS ✔ App Router

# 或手动安装到已有项目
npm install tailwindcss @tailwindcss/postcss postcss
/* app/globals.css —— Next.js 15 + Tailwind v4 */
@import "tailwindcss";

@theme {
  --font-sans: var(--font-geist-sans);  /* Next.js 内置字体 */
}
// app/layout.tsx
import './globals.css'
import { Geist } from 'next/font/google'

const geist = Geist({ subsets: ['latin'] })

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body className={cn(geist.className, 'bg-background text-foreground')}>
        {children}
      </body>
    </html>
  )
}

5. VS Code IntelliSense 插件配置

安装 Tailwind CSS IntelliSense 扩展(bradlc.vscode-tailwindcss)可以获得自动补全、悬停预览、语法检查等功能。

// .vscode/settings.json
{
  // 让 cn() 函数内也有自动补全
  "tailwindCSS.experimental.classRegex": [
    ["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["clsx\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
  ],
  // 关联文件类型
  "tailwindCSS.includeLanguages": {
    "plaintext": "html"
  }
}

本章小结:React 项目中,cn(clsx + tailwind-merge)是动态类名的最佳工具;Vue 用 :class 对象/数组语法;Next.js 项目在创建时选择 Tailwind 选项即可自动配置。VS Code 安装 IntelliSense 插件后,Tailwind 的开发体验接近原生 CSS 编辑器。