从零到一:构建高性能React组件库的完整指南

5 阅读1分钟

引言:为什么需要自建组件库?

在现代前端开发中,组件库已成为提升开发效率、保证产品一致性的关键工具。虽然市面上有Ant Design、Material-UI等优秀的开源组件库,但在实际项目中,我们常常会遇到以下痛点:

  1. 样式定制困难:现有组件库的样式体系与品牌设计语言不匹配
  2. 性能开销大:引入完整组件库导致包体积膨胀
  3. 功能冗余:80%的功能用不到,却要为20%的核心功能买单
  4. 维护成本高:第三方库的版本升级可能带来破坏性变更

本文将带你从零开始,构建一个高性能、可维护、易扩展的React组件库,涵盖架构设计、开发规范、性能优化等核心内容。

一、架构设计与技术选型

1.1 项目结构规划

my-component-library/
├── packages/
│   ├── core/           # 核心工具和样式
│   ├── button/         # 按钮组件
│   ├── input/          # 输入框组件
│   └── modal/          # 模态框组件
├── docs/               # 文档网站
├── examples/           # 使用示例
└── scripts/            # 构建脚本

1.2 技术栈选择

{
  "react": "^18.0.0",           // React核心
  "typescript": "^5.0.0",       // 类型安全
  "rollup": "^4.0.0",           // 打包工具
  "jest": "^29.0.0",            // 单元测试
  "storybook": "^7.0.0",        // 组件文档和开发环境
  "postcss": "^8.0.0",          // CSS处理
  "tailwindcss": "^3.0.0"       // 原子化CSS(可选)
}

二、搭建开发环境

2.1 初始化项目

# 创建项目目录
mkdir my-component-library
cd my-component-library

# 初始化monorepo
npm init -y

# 安装基础依赖
npm install -D typescript @types/react @types/react-dom
npm install react react-dom

2.2 配置TypeScript

// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "module": "ESNext",
    "jsx": "react-jsx",
    "declaration": true,
    "declarationDir": "dist/types",
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
}

2.3 配置Rollup打包

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import postcss from 'rollup-plugin-postcss';
import { terser } from 'rollup-plugin-terser';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm',
      sourcemap: true
    }
  ],
  plugins: [
    peerDepsExternal(),
    resolve(),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.json',
      declaration: true,
      declarationDir: 'dist/types'
    }),
    postcss({
      modules: true,
      extract: true,
      minimize: true
    }),
    terser()
  ],
  external: ['react', 'react-dom']
};

三、实现核心组件

3.1 基础Button组件

// packages/button/src/Button.tsx
import React, { forwardRef, ButtonHTMLAttributes } from 'react';
import classNames from 'classnames';
import styles from './Button.module.css';

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

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** 按钮变体 */
  variant?: ButtonVariant;
  /** 按钮尺寸 */
  size?: ButtonSize;
  /** 是否加载中 */
  loading?: boolean;
  /** 是否禁用 */
  disabled?: 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,
      icon,
      className,
      onClick,
      ...rest
    },
    ref
  ) => {
    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      if (!loading && !disabled && onClick) {
        onClick(event);
      }
    };

    const buttonClasses = classNames(
      styles.button,
      styles[`button--${variant}`],
      styles[`button--${size}`],
      {
        [styles['button--loading']]: loading,
        [styles['button--disabled']]: disabled,
      },
      className
    );

    return (
      <button
        ref={ref}
        className={buttonClasses}
        disabled={disabled || loading}
        onClick={handleClick}
        aria-busy={loading}
        {...rest}
      >
        {loading && (
          <span className={styles.loadingSpinner} aria-hidden="true" />
        )}
        {icon && !loading && (
          <span className={styles.buttonIcon}>{icon}</span>
        )}
        <span className={styles.buttonContent}>{children}</span>
      </button>
    );
  }
);

Button.displayName = 'Button';

3.2 样式模块

/* packages/button/src/Button.module.css */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  position: relative;
  user-select: none;
  outline: none;
}

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

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

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

/* 变体 */
.button--primary {
  background-color: #007bff;
  color: white;
}

.button--primary:hover:not(.button--disabled) {
  background-color: #0056b3;
}

.button--secondary {
  background-color: #6c757d;
  color: white;
}

.button--secondary:hover:not(.button--disabled) {
  background-color: #545b62;
}

/* 加载状态 */
.button--loading {
  cursor: wait;
  opacity: 0.7;
}

.loadingSpinner {
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  border-top-color: white;
  animation: spin 1s ease-in-out infinite;
  margin-right: 8px;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* 禁用状态 */
.button--disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

3.3 高级组件:Modal实现

// packages/modal/src/Modal.tsx
import React, { useEffect, useCallback } from 'react';
import ReactDOM from 'react-dom';
import { Button } from '@my-library/button';
import styles from './Modal.module.css';

interface ModalProps {
  /** 是否显示 */
  isOpen: boolean;
  /** 标题 */
  title?: string;
  /** 内容 */
  children: React.ReactNode;
  /** 确认按钮文本 */
  confirmText?: string;
  /** 取消按钮文本 */
  cancelText?: string