开发一个基于 Tailwind CSS 的组件库不仅能提高团队开发效率,还能确保产品的设计一致性。本节将详细介绍如何从零开始构建一个专业的组件库。
!!! note
我们使用 React 来做项目相关的演示。
!!!
项目初始化
基础配置
# 创建项目
mkdir my-component-library
cd my-component-library
pnpm init# 安装依赖
pnpm add -D tailwindcss postcss autoprefixer typescript
pnpm add -D @types/react @types/react-dom
pnpm add -D vite @vitejs/plugin-react
pnpm 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.tsx
import 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.ts
export 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.ts
export 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.ts
import { 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.js
module.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.tsx
import 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.tsx
import { 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.ts
import { 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.yml
name: 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 支持
- 样式优化
- 包体积控制
-
维护策略
- 版本控制
- 更新日志
- 问题跟踪
- 文档更新