开发一个基于 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 ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={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: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test
- name: Build
run: 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 支持
- 样式优化
- 包体积控制
维护策略
- 版本控制
- 更新日志
- 问题跟踪
- 文档更新