开发一个基于 Tailwind CSS 的组件库不仅能提高团队开发效率,还能确保产品的设计一致性。本节将详细介绍如何从零开始构建一个专业的组件库。
!!! note 我们使用 React 来做项目相关的演示。 !!!
项目初始化
基础配置
# 创建项目mkdir my-component-librarycd my-component-librarypnpm init# 安装依赖pnpm add -D tailwindcss postcss autoprefixer typescriptpnpm add -D @types/react @types/react-dompnpm add -D vite @vitejs/plugin-reactpnpm add -D tsup# 安装 peer 依赖pnpm add -D react react-dom
项目结构
src/├── components/│ ├── Button/│ │ ├── Button.tsx│ │ ├── Button.test.tsx│ │ └── index.ts│ ├── Input/│ └── Select/├── hooks/│ └── useTheme.ts├── styles/│ ├── base.css│ └── themes/├── utils/│ └── className.ts└── index.ts
组件开发规范
组件基础模板
// src/components/Button/Button.tsximport React from 'react';import { cva, type VariantProps } from 'class-variance-authority';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-offset-2 disabled:opacity-50 disabled:pointer-events-none',{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',},size: {default: 'h-10 py-2 px-4',sm: 'h-9 px-3 rounded-md',lg: 'h-11 px-8 rounded-md',},},defaultVariants: {variant: 'default',size: 'default',},});export interface ButtonPropsextends React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof buttonVariants> {}const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, ...props }, ref) => {return (<buttonclassName={buttonVariants({ variant, size, className })}ref={ref}{...props}/>);});Button.displayName = 'Button';export { Button, buttonVariants };
类型定义
// src/types/components.tsexport type Variant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';export type Size = 'default' | 'sm' | 'lg';export interface BaseProps {className?: string;children?: React.ReactNode;}export interface WithVariants {variant?: Variant;size?: Size;}
样式系统
主题配置
// src/styles/theme.tsexport const theme = {colors: {primary: {DEFAULT: 'hsl(222.2, 47.4%, 11.2%)',foreground: 'hsl(210, 40%, 98%)',},secondary: {DEFAULT: 'hsl(210, 40%, 96.1%)',foreground: 'hsl(222.2, 47.4%, 11.2%)',},destructive: {DEFAULT: 'hsl(0, 84.2%, 60.2%)',foreground: 'hsl(210, 40%, 98%)',},// ...其他颜色},spacing: {// ...间距配置},borderRadius: {// ...圆角配置},};
样式工具
// src/utils/className.tsimport { clsx, type ClassValue } from 'clsx';import { twMerge } from 'tailwind-merge';export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}// 使用示例const className = cn('base-style',variant === 'primary' && 'primary-style',className);
组件文档
Storybook 配置
// .storybook/main.jsmodule.exports = {stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],addons: ['@storybook/addon-links','@storybook/addon-essentials','@storybook/addon-interactions','@storybook/addon-a11y',],framework: {name: '@storybook/react-vite',options: {},},};
组件文档示例
// src/components/Button/Button.stories.tsximport type { Meta, StoryObj } from '@storybook/react';import { Button } from './Button';const meta = {title: 'Components/Button',component: Button,parameters: {layout: 'centered',},tags: ['autodocs'],} satisfies Meta<typeof Button>;export default meta;type Story = StoryObj<typeof meta>;export const Primary: Story = {args: {children: 'Button',variant: 'default',},};export const Secondary: Story = {args: {children: 'Button',variant: 'secondary',},};
测试规范
单元测试配置
// src/components/Button/Button.test.tsximport { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { Button } from './Button';describe('Button', () => {it('renders correctly', () => {render(<Button>Click me</Button>);expect(screen.getByRole('button')).toHaveTextContent('Click me');});it('handles click events', async () => {const handleClick = jest.fn();render(<Button onClick={handleClick}>Click me</Button>);await userEvent.click(screen.getByRole('button'));expect(handleClick).toHaveBeenCalled();});it('applies variant styles correctly', () => {render(<Button variant="destructive">Delete</Button>);expect(screen.getByRole('button')).toHaveClass('bg-destructive');});});
构建和发布
构建配置
// tsup.config.tsimport { defineConfig } from 'tsup';export default defineConfig({entry: ['src/index.ts'],format: ['cjs', 'esm'],dts: true,splitting: false,sourcemap: true,clean: true,external: ['react', 'react-dom'],injectStyle: false,});
包配置
{"name": "@your-org/components","version": "1.0.0","main": "./dist/index.js","module": "./dist/index.mjs","types": "./dist/index.d.ts","sideEffects": false,"files": ["dist/**"],"scripts": {"build": "tsup","dev": "tsup --watch","lint": "eslint src/","test": "jest","storybook": "storybook dev -p 6006","build-storybook": "storybook build"},"peerDependencies": {"react": "^18.0.0","react-dom": "^18.0.0"}}
CI/CD 配置
GitHub Actions
# .github/workflows/ci.ymlname: CIon:push:branches: [main]pull_request:branches: [main]jobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v2- uses: pnpm/action-setup@v2- uses: actions/setup-node@v2with:node-version: '18'cache: 'pnpm'- name: Install dependenciesrun: pnpm install --frozen-lockfile- name: Lintrun: pnpm lint- name: Testrun: pnpm test- name: Buildrun: pnpm build
版本管理
Changesets 配置
// .changeset/config.json{"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json","changelog": "@changesets/cli/changelog","commit": false,"fixed": [],"linked": [],"access": "restricted","baseBranch": "main","updateInternalDependencies": "patch","ignore": []}
最佳实践
组件设计原则
- 组件职责单一
- 接口设计合理
- 样式可定制
- 可访问性支持
开发流程
- 文档先行
- TDD 开发
- 代码审查
- 持续集成
性能优化
- 按需加载
- Tree-shaking 支持
- 样式优化
- 包体积控制
维护策略
- 版本控制
- 更新日志
- 问题跟踪
- 文档更新
