开发一个基于 Tailwind CSS 的组件库不仅能提高团队开发效率,还能确保产品的设计一致性。本节将详细介绍如何从零开始构建一个专业的组件库。

!!! note 我们使用 React 来做项目相关的演示。 !!!

项目初始化

基础配置

  1. # 创建项目
  2. mkdir my-component-library
  3. cd my-component-library
  4. pnpm init
  5. # 安装依赖
  6. pnpm add -D tailwindcss postcss autoprefixer typescript
  7. pnpm add -D @types/react @types/react-dom
  8. pnpm add -D vite @vitejs/plugin-react
  9. pnpm add -D tsup
  10. # 安装 peer 依赖
  11. pnpm add -D react react-dom

项目结构

  1. src/
  2. ├── components/
  3. ├── Button/
  4. ├── Button.tsx
  5. ├── Button.test.tsx
  6. └── index.ts
  7. ├── Input/
  8. └── Select/
  9. ├── hooks/
  10. └── useTheme.ts
  11. ├── styles/
  12. ├── base.css
  13. └── themes/
  14. ├── utils/
  15. └── className.ts
  16. └── index.ts

组件开发规范

组件基础模板

  1. // src/components/Button/Button.tsx
  2. import React from 'react';
  3. import { cva, type VariantProps } from 'class-variance-authority';
  4. const buttonVariants = cva(
  5. // 基础样式
  6. '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',
  7. {
  8. variants: {
  9. variant: {
  10. default: 'bg-primary text-primary-foreground hover:bg-primary/90',
  11. destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
  12. outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
  13. secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
  14. ghost: 'hover:bg-accent hover:text-accent-foreground',
  15. link: 'underline-offset-4 hover:underline text-primary',
  16. },
  17. size: {
  18. default: 'h-10 py-2 px-4',
  19. sm: 'h-9 px-3 rounded-md',
  20. lg: 'h-11 px-8 rounded-md',
  21. },
  22. },
  23. defaultVariants: {
  24. variant: 'default',
  25. size: 'default',
  26. },
  27. }
  28. );
  29. export interface ButtonProps
  30. extends React.ButtonHTMLAttributes<HTMLButtonElement>,
  31. VariantProps<typeof buttonVariants> {}
  32. const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  33. ({ className, variant, size, ...props }, ref) => {
  34. return (
  35. <button
  36. className={buttonVariants({ variant, size, className })}
  37. ref={ref}
  38. {...props}
  39. />
  40. );
  41. }
  42. );
  43. Button.displayName = 'Button';
  44. export { Button, buttonVariants };

类型定义

  1. // src/types/components.ts
  2. export type Variant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
  3. export type Size = 'default' | 'sm' | 'lg';
  4. export interface BaseProps {
  5. className?: string;
  6. children?: React.ReactNode;
  7. }
  8. export interface WithVariants {
  9. variant?: Variant;
  10. size?: Size;
  11. }

样式系统

主题配置

  1. // src/styles/theme.ts
  2. export const theme = {
  3. colors: {
  4. primary: {
  5. DEFAULT: 'hsl(222.2, 47.4%, 11.2%)',
  6. foreground: 'hsl(210, 40%, 98%)',
  7. },
  8. secondary: {
  9. DEFAULT: 'hsl(210, 40%, 96.1%)',
  10. foreground: 'hsl(222.2, 47.4%, 11.2%)',
  11. },
  12. destructive: {
  13. DEFAULT: 'hsl(0, 84.2%, 60.2%)',
  14. foreground: 'hsl(210, 40%, 98%)',
  15. },
  16. // ...其他颜色
  17. },
  18. spacing: {
  19. // ...间距配置
  20. },
  21. borderRadius: {
  22. // ...圆角配置
  23. },
  24. };

样式工具

  1. // src/utils/className.ts
  2. import { clsx, type ClassValue } from 'clsx';
  3. import { twMerge } from 'tailwind-merge';
  4. export function cn(...inputs: ClassValue[]) {
  5. return twMerge(clsx(inputs));
  6. }
  7. // 使用示例
  8. const className = cn(
  9. 'base-style',
  10. variant === 'primary' && 'primary-style',
  11. className
  12. );

组件文档

Storybook 配置

  1. // .storybook/main.js
  2. module.exports = {
  3. stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  4. addons: [
  5. '@storybook/addon-links',
  6. '@storybook/addon-essentials',
  7. '@storybook/addon-interactions',
  8. '@storybook/addon-a11y',
  9. ],
  10. framework: {
  11. name: '@storybook/react-vite',
  12. options: {},
  13. },
  14. };

组件文档示例

  1. // src/components/Button/Button.stories.tsx
  2. import type { Meta, StoryObj } from '@storybook/react';
  3. import { Button } from './Button';
  4. const meta = {
  5. title: 'Components/Button',
  6. component: Button,
  7. parameters: {
  8. layout: 'centered',
  9. },
  10. tags: ['autodocs'],
  11. } satisfies Meta<typeof Button>;
  12. export default meta;
  13. type Story = StoryObj<typeof meta>;
  14. export const Primary: Story = {
  15. args: {
  16. children: 'Button',
  17. variant: 'default',
  18. },
  19. };
  20. export const Secondary: Story = {
  21. args: {
  22. children: 'Button',
  23. variant: 'secondary',
  24. },
  25. };

测试规范

单元测试配置

  1. // src/components/Button/Button.test.tsx
  2. import { render, screen } from '@testing-library/react';
  3. import userEvent from '@testing-library/user-event';
  4. import { Button } from './Button';
  5. describe('Button', () => {
  6. it('renders correctly', () => {
  7. render(<Button>Click me</Button>);
  8. expect(screen.getByRole('button')).toHaveTextContent('Click me');
  9. });
  10. it('handles click events', async () => {
  11. const handleClick = jest.fn();
  12. render(<Button onClick={handleClick}>Click me</Button>);
  13. await userEvent.click(screen.getByRole('button'));
  14. expect(handleClick).toHaveBeenCalled();
  15. });
  16. it('applies variant styles correctly', () => {
  17. render(<Button variant="destructive">Delete</Button>);
  18. expect(screen.getByRole('button')).toHaveClass('bg-destructive');
  19. });
  20. });

构建和发布

构建配置

  1. // tsup.config.ts
  2. import { defineConfig } from 'tsup';
  3. export default defineConfig({
  4. entry: ['src/index.ts'],
  5. format: ['cjs', 'esm'],
  6. dts: true,
  7. splitting: false,
  8. sourcemap: true,
  9. clean: true,
  10. external: ['react', 'react-dom'],
  11. injectStyle: false,
  12. });

包配置

  1. {
  2. "name": "@your-org/components",
  3. "version": "1.0.0",
  4. "main": "./dist/index.js",
  5. "module": "./dist/index.mjs",
  6. "types": "./dist/index.d.ts",
  7. "sideEffects": false,
  8. "files": [
  9. "dist/**"
  10. ],
  11. "scripts": {
  12. "build": "tsup",
  13. "dev": "tsup --watch",
  14. "lint": "eslint src/",
  15. "test": "jest",
  16. "storybook": "storybook dev -p 6006",
  17. "build-storybook": "storybook build"
  18. },
  19. "peerDependencies": {
  20. "react": "^18.0.0",
  21. "react-dom": "^18.0.0"
  22. }
  23. }

CI/CD 配置

GitHub Actions

  1. # .github/workflows/ci.yml
  2. name: CI
  3. on:
  4. push:
  5. branches: [main]
  6. pull_request:
  7. branches: [main]
  8. jobs:
  9. test:
  10. runs-on: ubuntu-latest
  11. steps:
  12. - uses: actions/checkout@v2
  13. - uses: pnpm/action-setup@v2
  14. - uses: actions/setup-node@v2
  15. with:
  16. node-version: '18'
  17. cache: 'pnpm'
  18. - name: Install dependencies
  19. run: pnpm install --frozen-lockfile
  20. - name: Lint
  21. run: pnpm lint
  22. - name: Test
  23. run: pnpm test
  24. - name: Build
  25. run: pnpm build

版本管理

Changesets 配置

  1. // .changeset/config.json
  2. {
  3. "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
  4. "changelog": "@changesets/cli/changelog",
  5. "commit": false,
  6. "fixed": [],
  7. "linked": [],
  8. "access": "restricted",
  9. "baseBranch": "main",
  10. "updateInternalDependencies": "patch",
  11. "ignore": []
  12. }

最佳实践

  1. 组件设计原则

    • 组件职责单一
    • 接口设计合理
    • 样式可定制
    • 可访问性支持
  2. 开发流程

    • 文档先行
    • TDD 开发
    • 代码审查
    • 持续集成
  3. 性能优化

    • 按需加载
    • Tree-shaking 支持
    • 样式优化
    • 包体积控制
  4. 维护策略

    • 版本控制
    • 更新日志
    • 问题跟踪
    • 文档更新