"我们真的需要自己写一套组件库吗?"上周的技术评审会上,我正在和团队讨论组件库的选型。作为一个快速发展的创业公司,我们既需要高质量的组件,又想保持灵活的定制能力。在对比了多个方案后,我们选择了 shadcn/ui 这个相对较新的解决方案。
说实话,最开始我对这个决定也有些担忧。毕竟相比 Ant Design 这样的成熟方案,shadcn/ui 的知名度确实不高。但经过一个月的实践,这个选择让我们收获了意外的惊喜。
为什么选择 shadcn/ui?
传统组件库给我们带来了一些困扰:
- 样式难以深度定制
- 打包体积大
- 版本升级困难
- 组件逻辑难以调整
就像租房和买房的选择一样,使用第三方组件库就像租房,虽然能快速入住,但想改造却处处受限。而 shadcn/ui 的方案,更像是买了一栋毛坯房,虽然需要自己装修,但能完全掌控每个细节。
项目实践
1. 初始化配置
首先,我们需要建立一个良好的组件开发基础:
// components.json
{"$schema": "https://ui.shadcn.com/schema.json","style": "default","rsc": true,"tailwind": {"config": "tailwind.config.js","css": "app/globals.css","baseColor": "slate","cssVariables": true},"aliases": {"components": "@/components","utils": "@/lib/utils"}
}// tailwind.config.js
const { fontFamily } = require('tailwindcss/defaultTheme')/** @type {import('tailwindcss').Config} */
module.exports = {darkMode: ['class'],content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],theme: {container: {center: true,padding: '2rem',screens: {'2xl': '1400px'}},extend: {colors: {border: 'hsl(var(--border))',input: 'hsl(var(--input))',ring: 'hsl(var(--ring))',background: 'hsl(var(--background))',foreground: 'hsl(var(--foreground))',primary: {DEFAULT: 'hsl(var(--primary))',foreground: 'hsl(var(--primary-foreground))'}},borderRadius: {lg: 'var(--radius)',md: 'calc(var(--radius) - 2px)',sm: 'calc(var(--radius) - 4px)'},fontFamily: {sans: ['var(--font-sans)', ...fontFamily.sans]}}}
}
2. 组件定制
shadcn/ui 最大的特点是它的组件是可以复制到项目中的。这让我们能够根据业务需求进行深度定制:
// components/ui/button.tsx
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'// 扩展按钮变体
const buttonVariants = cva('inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', {variants: {variant: {default: 'bg-primary text-primary-foreground hover:bg-primary/90',destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',outline: 'border border-input hover:bg-accent hover:text-accent-foreground',secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',ghost: 'hover:bg-accent hover:text-accent-foreground',link: 'underline-offset-4 hover:underline text-primary',// 添加自定义变体brand: 'bg-brand-500 text-white hover:bg-brand-600',success: 'bg-green-500 text-white hover:bg-green-600'},size: {default: 'h-10 px-4 py-2',sm: 'h-9 rounded-md px-3',lg: 'h-11 rounded-md px-8',icon: 'h-10 w-10'}},defaultVariants: {variant: 'default',size: 'default'}
})// 扩展按钮属性
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {asChild?: booleanloading?: booleanicon?: React.ReactNode
}const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, loading, icon, children, ...props }, ref) => {const Comp = asChild ? Slot : 'button'return (<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props}>{loading && <LoadingSpinner className='mr-2 h-4 w-4 animate-spin' />}{icon && <span className='mr-2'>{icon}</span>}{children}</Comp>)
})
Button.displayName = 'Button'
3. 主题定制
我们实现了一个灵活的主题切换系统:
// lib/themes.ts
export const themes = {light: {'--background': '0 0% 100%','--foreground': '222.2 84% 4.9%','--primary': '222.2 47.4% 11.2%','--primary-foreground': '210 40% 98%'// ... 其他颜色变量},dark: {'--background': '222.2 84% 4.9%','--foreground': '210 40% 98%','--primary': '210 40% 98%','--primary-foreground': '222.2 47.4% 11.2%'// ... 其他颜色变量},// 添加自定义主题brand: {'--background': '0 0% 100%','--foreground': '222.2 84% 4.9%','--primary': '220 90% 56%','--primary-foreground': '210 40% 98%'}
}// components/theme-provider.tsx
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {const [theme, setTheme] = useState('light')useEffect(() => {const root = document.documentElementconst themeVars = themes[theme as keyof typeof themes]Object.entries(themeVars).forEach(([key, value]) => {root.style.setProperty(key, value)})}, [theme])return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>
}
4. 组件封装
基于 shadcn/ui 的基础组件,我们封装了一些业务组件:
// components/business/data-table.tsx
import { Table } from '@/components/ui/table'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useState } from 'react'interface DataTableProps<T> {data: T[]columns: Column[]onEdit?: (record: T) => voidonDelete?: (record: T) => void
}export function DataTable<T>({ data, columns, onEdit, onDelete }: DataTableProps<T>) {const [searchText, setSearchText] = useState('')const filteredData = data.filter(record =>columns.some(column => {const value = record[column.key as keyof T]return String(value).toLowerCase().includes(searchText.toLowerCase())}))return (<div className='space-y-4'><div className='flex justify-between'><Input placeholder='搜索...' value={searchText} onChange={e => setSearchText(e.target.value)} className='max-w-sm' /></div><Table><Table.Header>{columns.map(column => (<Table.Column key={column.key}>{column.title}</Table.Column>))}<Table.Column>操作</Table.Column></Table.Header><Table.Body>{filteredData.map((record, index) => (<Table.Row key={index}>{columns.map(column => (<Table.Cell key={column.key}>{record[column.key as keyof T]}</Table.Cell>))}<Table.Cell><div className='space-x-2'>{onEdit && (<Button size='sm' variant='outline' onClick={() => onEdit(record)}>编辑</Button>)}{onDelete && (<Button size='sm' variant='destructive' onClick={() => onDelete(record)}>删除</Button>)}</div></Table.Cell></Table.Row>))}</Table.Body></Table></div>)
}
实践效果
经过一个月的使用,我们获得了显著的收益:
- 打包体积减少了 60%
- 组件定制更加灵活
- 开发效率提升
- 代码可维护性增强
最让我印象深刻的是一位同事说:"终于可以随心所欲地修改组件了,不用再为覆盖第三方样式发愁。"
经验总结
使用 shadcn/ui 的过程让我们学到:
- 组件库不一定要追求"拿来即用"
- 掌控组件源码比黑盒封装更有优势
- 样式系统的一致性很重要
- 渐进式地构建组件库是个好方法
就像装修房子,虽然前期投入较大,但最终得到的是一个完全符合需求的解决方案。
写在最后
shadcn/ui 给了我们一个全新的视角:组件库可以是一系列最佳实践的集合,而不仅仅是一个封装好的产品。正如那句话说的:"授人以鱼不如授人以渔",shadcn/ui 不仅给了我们组件,更教会了我们如何构建组件。
有什么问题欢迎在评论区讨论,让我们一起探讨组件库开发的更多可能!
如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~