从零到一:构建一个现代化的 React 组件库

在当今的前端开发中,组件化已经成为构建用户界面的标准范式。无论是大型企业应用还是个人项目,拥有一个设计一致、可复用性高的组件库都能显著提升开发效率和代码质量。本文将带你从零开始,构建一个现代化的 React 组件库,涵盖架构设计、开发工具、最佳实践和发布流程。

为什么需要自建组件库?

在开始之前,我们先明确自建组件库的价值:

  1. 设计一致性:确保整个应用的设计语言统一
  2. 开发效率:避免重复造轮子,专注业务逻辑
  3. 维护性:一处修改,处处更新
  4. 团队协作:提供标准的开发范式,降低沟通成本
  5. 质量保证:统一的测试和文档标准

技术选型与架构设计

核心依赖

json 复制代码
{
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "typescript": "^5.0.0",
  "@types/react": "^18.0.0",
  "@types/react-dom": "^18.0.0"
}

构建工具链

现代组件库需要一套完整的工具链支持:

json 复制代码
{
  "vite": "^4.0.0",          // 构建工具
  "storybook": "^7.0.0",     // 组件文档和开发环境
  "jest": "^29.0.0",         // 单元测试
  "@testing-library/react": "^14.0.0",
  "eslint": "^8.0.0",        // 代码检查
  "prettier": "^3.0.0",      // 代码格式化
  "husky": "^8.0.0",         // Git hooks
  "lint-staged": "^13.0.0"
}

项目初始化与配置

1. 创建项目结构

perl 复制代码
my-component-library/
├── src/
│   ├── components/         # 组件源码
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.test.tsx
│   │   │   ├── Button.stories.tsx
│   │   │   └── index.ts
│   │   └── index.ts       # 组件导出
│   ├── styles/            # 样式文件
│   │   ├── variables.scss
│   │   └── mixins.scss
│   └── utils/             # 工具函数
├── stories/               # Storybook 配置
├── tests/                 # 测试配置
├── .eslintrc.js
├── .prettierrc
├── tsconfig.json
├── vite.config.ts
└── package.json

2. 配置 TypeScript

json 复制代码
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "declaration": true,
    "declarationDir": "dist/types",
    "outDir": "dist",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.tsx", "**/*.stories.tsx"]
}

3. 配置 Vite 构建

typescript 复制代码
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    react(),
    dts({
      insertTypesEntry: true,
    }),
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/components/index.ts'),
      name: 'MyComponentLibrary',
      formats: ['es', 'umd'],
      fileName: (format) => `my-component-library.${format}.js`,
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
})

开发第一个组件:Button

1. 组件实现

typescript 复制代码
// src/components/Button/Button.tsx
import React, { forwardRef, ButtonHTMLAttributes } from 'react'
import classNames from 'classnames'
import './Button.scss'

export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'
export type ButtonSize = 'small' | 'medium' | 'large'

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** 按钮变体 */
  variant?: ButtonVariant
  /** 按钮尺寸 */
  size?: ButtonSize
  /** 是否加载中 */
  loading?: boolean
  /** 是否禁用 */
  disabled?: boolean
  /** 是否块级元素 */
  block?: boolean
  /** 图标 */
  icon?: React.ReactNode
  /** 点击事件 */
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      children,
      variant = 'primary',
      size = 'medium',
      loading = false,
      disabled = false,
      block = false,
      icon,
      className,
      onClick,
      ...rest
    },
    ref
  ) => {
    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      if (loading || disabled) return
      onClick?.(event)
    }

    const classes = classNames(
      'btn',
      `btn--${variant}`,
      `btn--${size}`,
      {
        'btn--loading': loading,
        'btn--disabled': disabled,
        'btn--block': block,
      },
      className
    )

    return (
      <button
        ref={ref}
        className={classes}
        disabled={disabled || loading}
        onClick={handleClick}
        aria-busy={loading}
        {...rest}
      >
        {loading && (
          <span className="btn__loader" aria-hidden="true">
            <svg className="btn__loader-svg" viewBox="0 0 50 50">
              <circle className="btn__loader-circle" cx="25" cy="25" r="20" />
            </svg>
          </span>
        )}
        {icon && !loading && <span className="btn__icon">{icon}</span>}
        <span className="btn__content">{children}</span>
      </button>
    )
  }
)

Button.displayName = 'Button'

2. 样式实现(使用 Sass)

scss 复制代码
// src/components/Button/Button.scss
@import '../../styles/variables';

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 4px;
  font-family: inherit;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  user-select: none;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;
  outline: none;

  &:focus-visible {
    box-shadow: 0 0 0 3px rgba($primary-color, 0.3);
  }

  // 尺寸
  &--small {
    padding: 6px 12px;
    font-size: 12px;
    line-height: 1.5;
  }

  &--medium {
    padding: 8px 16px;
    font-size: 14px;
    line-height: 1.5;
  }

  &--large {
    padding: 12px 24px;
    font-size: 16px;
    line-height: 1.5;
  }

  // 变体
  &--primary {
    background-color: $primary-color;
    color: white;

    &:hover:not(:disabled) {
      background-color: darken($primary-color, 10%);
    }

    &:active:not(:disabled) {
      background-color: darken($primary-color, 15%);
    }
  }

  &--secondary {
    background-color: $secondary-color;
    color: $text-color;

    &:hover:not(:disabled) {
      background-color: darken($secondary-color, 10%
相关推荐
用户020742201752 小时前
从零到一:用 Rust 和 WebAssembly 构建高性能前端应用
后端
用户020742201752 小时前
从零到一:构建你的第一个智能合约并部署到以太坊测试网
后端
掘金者阿豪2 小时前
数据库的第一道防线:从金仓KES看企业级身份验证体系的设计逻辑
后端
颜酱2 小时前
从0到1实现LFU缓存:思路拆解+代码落地
javascript·后端·算法
武子康2 小时前
大数据-241 离线数仓 - 实战:电商核心交易数据模型与 MySQL 源表设计(订单/商品/品类/店铺/支付)
大数据·后端·mysql
SimonKing2 小时前
JetBrains 用户狂喜!这个 AI 插件让 IDE 原地进化成「智能编码助手」
java·后端·程序员
茶杯梦轩2 小时前
从零起步学习RabbitMQ || 第三章:RabbitMQ的生产者、Broker、消费者如何保证消息不丢失(可靠性)详解
分布式·后端·面试
小码哥_常2 小时前
别再乱加exclusion了!Maven依赖冲突有妙解
后端
狂奔小菜鸡2 小时前
Day39 | Java中更灵活的锁ReentrantLock
java·后端·java ee