12k
All articles

为什么开发者在 React 项目中转向 shadcn/ui

对比 shadcn/ui 与 Material-UI、Chakra UI,介绍其 CLI 脚手架、Radix UI 原语及 Tailwind CSS 如何提升组件定制能力。

OpenReplay Team
OpenReplay Team
为什么开发者在 React 项目中转向 shadcn/ui

如果你一直在使用传统的 UI 库(如 Material-UI 或 Chakra UI)构建 React 应用程序,你可能遇到过同样的困扰:与主题覆盖作斗争、处理臃肿的打包体积,以及难以自定义不完全符合设计要求的组件。越来越多的开发者在 shadcn/ui 中找到了解决方案——这是一种根本不同的 React 组件方法,正在改变我们对 UI 库的思考方式。

本文解释了为什么 shadcn/ui React 采用率正在加速增长,其基于 CLI 的组件模型如何工作,以及何时适合你的项目。我们将直接与传统库进行比较,并探讨其优势和权衡。

核心要点

  • shadcn/ui 将组件源代码复制到你的项目中,而不是作为依赖项安装
  • 组件基于 Radix UI 原语构建以确保可访问性,使用 Tailwind CSS 进行样式设计
  • 这种方法提供完全的自定义控制并消除供应商锁定
  • 最适合自定义设计系统、复杂表单和仪表板应用程序
  • 需要 Tailwind CSS 专业知识和手动组件维护
  • 权衡包括设置复杂性和相比成熟库有限的组件生态系统

shadcn/ui 与传统 React 库的不同之处

与你作为 npm 包安装的传统 UI 库不同,shadcn/ui 基于一个根本不同的原则运作:代码所有权。你不是从 node_modules 导入预编译的组件,而是将实际的组件源代码直接复制到你的项目中。

以下是两种方法的区别:

传统库方法:

npm install @mui/material
import { Button } from '@mui/material'

shadcn/ui 方法:

npx shadcn-ui@latest add button
import { Button } from "@/components/ui/button"

关键区别?使用 shadcn/ui,Button 组件的源代码现在存在于你的 components/ui/ 目录中,而不是在 node_modules 中。你完全拥有它。

基于 CLI 的脚手架模型解释

shadcn/ui CLI 是这个系统的核心。当你运行 npx shadcn-ui@latest add button 时,它会:

  1. 下载 Button 组件的 TypeScript 源代码
  2. 将其放置在你指定的组件目录中
  3. 包含所有必要的依赖项和工具
  4. 配置适当的 TypeScript 类型

这不仅仅是复制粘贴——这是智能脚手架。CLI 处理:

  • 依赖解析:自动安装所需的包,如用于样式变体的 class-variance-authority
  • 配置管理:遵循你项目的 Tailwind CSS 设置和 TypeScript 配置
  • 文件组织:在项目间创建一致的文件夹结构
# 在你的项目中初始化 shadcn/ui
npx shadcn-ui@latest init

# 根据需要添加单个组件
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add form

技术基础:Radix UI、Tailwind CSS 和 TypeScript

shadcn/ui React 组件之所以如此出色,归结于它们的技术基础。每个组件都建立在三个支柱之上:

用于可访问性的 Radix UI 原语

Radix UI 提供无样式、可访问的组件原语。这意味着 shadcn/ui 组件继承了:

  • 键盘导航:Tab、方向键和 escape 键处理
  • 屏幕阅读器支持:适当的 ARIA 属性和语义 HTML
  • 焦点管理:逻辑焦点流和适当的焦点捕获
// shadcn/ui Dialog 组件使用 Radix Dialog 原语
import * as DialogPrimitive from "@radix-ui/react-dialog"

const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
// 通过 Tailwind CSS 添加样式

用于样式的 Tailwind CSS

每个 shadcn/ui 组件都专门使用 Tailwind CSS 工具类。这提供了:

  • 一致的设计令牌:颜色、间距和排版遵循你的 Tailwind 配置
  • 响应式设计:内置响应式修饰符无缝工作
  • 轻松自定义:直接编辑 Tailwind 类来更改样式
// 带有 Tailwind 样式的 Button 组件
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        outline: "border border-input bg-background hover:bg-accent",
      },
    },
  }
)

用于开发者体验的 TypeScript

所有组件都包含完整的 TypeScript 定义,提供:

  • 类型安全:在编译时捕获属性错误
  • IntelliSense:组件属性和变体的自动完成
  • 重构支持:安全的重命名和重构

shadcn/ui vs 传统 UI 库:技术比较

让我们在关键维度上检查 shadcn/ui 与成熟库的比较:

开发者控制和自定义

shadcn/ui:

  • 完全的源代码访问权限
  • 直接修改组件文件
  • 没有主题提供者复杂性
  • Tailwind 原生自定义

Material-UI/Chakra UI:

  • 主题覆盖系统
  • CSS-in-JS 抽象
  • 有限的组件内部访问
  • 复杂的自定义 API

打包体积和性能

shadcn/ui:

  • 只包含你实际使用的组件
  • 没有运行时主题处理
  • 最小的 JavaScript 开销
  • 默认更好的 tree-shaking

传统库:

  • 经常包含未使用的组件
  • 运行时主题计算
  • 更大的 JavaScript 包
  • Tree-shaking 效果因库而异

供应商锁定风险

shadcn/ui:

  • 组件成为你代码库的一部分
  • 不依赖于外部包更新
  • 采用后消除迁移风险

传统库:

  • 依赖于包维护者的决策
  • 主要版本中的破坏性更改
  • 迁移复杂性随时间增加

shadcn/ui 表现出色的实际用例

自定义设计系统

在构建独特设计系统时,shadcn/ui 提供了完美的起点。你可以:

  • 修改组件变体以匹配品牌指南
  • 添加自定义属性和行为
  • 在应用程序中保持一致性
  • 在自己的仓库中记录更改
// 为你的设计系统自定义 Button 变体
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium",
  {
    variants: {
      variant: {
        // 你的自定义品牌变体
        brand: "bg-gradient-to-r from-purple-600 to-blue-600 text-white hover:from-purple-700 hover:to-blue-700",
      },
    },
  }
)

表单密集型应用程序

对于具有复杂表单的应用程序,shadcn/ui 的表单组件与 React Hook FormZod 无缝集成:

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"

const formSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export function LoginForm() {
  const form = useForm({
    resolver: zodResolver(formSchema),
  })

  const onSubmit = (values) => {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>邮箱</FormLabel>
              <FormControl>
                <Input type="email" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">登录</Button>
      </form>
    </Form>
  )
}

仪表板和管理界面

仪表板应用程序受益于 shadcn/ui 的数据显示组件:

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"

export function UserDashboard({ users }) {
  return (
    <div className="space-y-6">
      <div className="grid gap-4 md:grid-cols-3">
        <Card>
          <CardHeader>
            <CardTitle>总用户数</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{users.length}</div>
          </CardContent>
        </Card>
      </div>
      
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>姓名</TableHead>
            <TableHead>状态</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {users.map((user) => (
            <TableRow key={user.id}>
              <TableCell>{user.name}</TableCell>
              <TableCell>
                <Badge variant={user.active ? "default" : "secondary"}>
                  {user.active ? "活跃" : "非活跃"}
                </Badge>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

权衡和考虑因素

维护开销

代码所有权伴随着责任。你需要:

  • 手动更新组件:没有来自包管理器的自动更新
  • 处理安全补丁:监控像 Radix UI 这样的依赖项的更新
  • 保持一致性:确保组件间的更改保持连贯

需要 Tailwind CSS 专业知识

shadcn/ui 假设你熟悉 Tailwind CSS。团队需要:

  • 理解工具优先的 CSS 原则
  • 了解 Tailwind 的响应式和状态修饰符
  • 熟悉自定义 Tailwind 配置

初始设置复杂性

入门需要比传统库更多的配置:

  • Tailwind CSS 设置和配置
  • 路径别名的 TypeScript 配置
  • 理解组件架构

有限的组件生态系统

与成熟的库相比,shadcn/ui 提供:

  • 更少的预构建组件
  • 更少的社区贡献组件
  • 需要从头构建复杂组件

在你的 React 项目中开始使用 shadcn/ui

这里是一个实用的设置指南:

# 创建一个带有 TypeScript 和 Tailwind 的新 Next.js 项目
npx create-next-app@latest my-app --typescript --tailwind --eslint

# 导航到你的项目
cd my-app

# 初始化 shadcn/ui
npx shadcn-ui@latest init

# 添加你的第一个组件
npx shadcn-ui@latest add button card form

初始化过程将:

  1. 配置你的 tailwind.config.js
  2. 添加必要的 CSS 变量
  3. 设置组件路径别名
  4. 创建基本文件夹结构

结论

shadcn/ui 代表了 React UI 开发中向开发者赋权的转变。通过提供源代码所有权、可访问性优先的组件和 Tailwind CSS 集成,它解决了传统 UI 库的许多痛点。这种方法特别适用于自定义设计系统、表单密集型应用程序和熟悉 Tailwind CSS 的团队。

权衡——维护开销和所需的 Tailwind 专业知识——对大多数开发团队来说是可管理的。对于需要高度自定义、长期可维护性或摆脱供应商锁定的项目,shadcn/ui 相比传统组件库提供了令人信服的优势。

今天就通过访问官方文档并遵循安装指南开始使用 shadcn/ui 构建。CLI 使得在现有 React 项目中试验单个组件变得容易,无需承诺完全迁移。

常见问题

shadcn/ui 适合大型企业应用程序吗?

是的,shadcn/ui 非常适合企业应用程序,特别是那些需要自定义设计系统或严格可访问性要求的应用程序。代码所有权模型实际上相比依赖外部包更新减少了长期维护风险。

当发布新版本时,如何更新 shadcn/ui 组件?

你通过再次运行 CLI add 命令手动更新组件,这将显示差异。然后你可以选择接受更新或保留你的自定义。这让你完全控制何时以及如何更新。

可以在不使用 Tailwind CSS 的情况下使用 shadcn/ui 吗?

不可以,shadcn/ui 组件专门为 Tailwind CSS 构建。样式系统与 Tailwind 工具紧密集成。如果你偏好其他 CSS 方法,你需要完全重写组件样式。

如果 shadcn/ui 项目停止维护会怎么样?

由于组件存在于你的代码库中,你的应用程序继续正常工作。你拥有代码,可以根据需要维护、修改或替换组件,而不会有任何外部依赖项破坏你的应用程序。

shadcn/ui 与像 Reach UI 这样的无头 UI 库相比如何?

shadcn/ui 基于类似的可访问性原语(Radix UI)构建,但添加了使用 Tailwind CSS 的预样式组件。无头 UI 库给你更多样式灵活性,但需要更多工作来创建生产就绪的组件。shadcn/ui 提供了一个中间地带,具有良好的默认值和易于自定义。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.