"我们真的需要自己写一套组件库吗?"上周的技术评审会上,我正在和团队讨论组件库的选型。作为一个快速发展的创业公司,我们既需要高质量的组件,又想保持灵活的定制能力。在对比了多个方案后,我们选择了 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?: boolean
loading?: boolean
icon?: 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.documentElement
const 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) => void
onDelete?: (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 不仅给了我们组件,更教会了我们如何构建组件。
有什么问题欢迎在评论区讨论,让我们一起探讨组件库开发的更多可能!
如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~