"我们真的需要自己写一套组件库吗?"上周的技术评审会上,我正在和团队讨论组件库的选型。作为一个快速发展的创业公司,我们既需要高质量的组件,又想保持灵活的定制能力。在对比了多个方案后,我们选择了 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 && {icon}}
- {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 (
-
-
- <Input placeholder='搜索...' value={searchText} onChange={e => setSearchText(e.target.value)} className='max-w-sm' />
-
- <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>
-
- {onEdit && (
- <Button size='sm' variant='outline' onClick={() => onEdit(record)}>
- 编辑
- </Button>
- )}
- {onDelete && (
- <Button size='sm' variant='destructive' onClick={() => onDelete(record)}>
- 删除
- </Button>
- )}
-
- </Table.Cell>
- </Table.Row>
- ))}
- </Table.Body>
- </Table>
-
- )
- }
复制代码 实践效果
经过一个月的使用,我们获得了显著的收益:
- 打包体积减少了 60%
- 组件定制更加灵活
- 开发效率提升
- 代码可维护性增强
最让我印象深刻的是一位同事说:"终于可以随心所欲地修改组件了,不用再为覆盖第三方样式发愁。"
经验总结
使用 shadcn/ui 的过程让我们学到:
- 组件库不一定要追求"拿来即用"
- 掌控组件源码比黑盒封装更有优势
- 样式系统的一致性很重要
- 渐进式地构建组件库是个好方法
就像装修房子,虽然前期投入较大,但最终得到的是一个完全符合需求的解决方案。
写在最后
shadcn/ui 给了我们一个全新的视角:组件库可以是一系列最佳实践的集合,而不仅仅是一个封装好的产品。正如那句话说的:"授人以鱼不如授人以渔",shadcn/ui 不仅给了我们组件,更教会了我们如何构建组件。
有什么问题欢迎在评论区讨论,让我们一起探讨组件库开发的更多可能!
如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |