引言:为什么需要自建组件库?
在现代前端开发中,组件库已成为提升开发效率、保证产品一致性的关键工具。虽然市面上有Ant Design、Material-UI等优秀的开源组件库,但在实际企业级项目中,自建组件库往往能更好地满足特定业务需求、设计规范和性能要求。
本文将带你从零开始,构建一个高性能、可维护的React组件库,涵盖架构设计、开发规范、性能优化到发布部署的全流程。
一、架构设计与技术选型
1.1 项目初始化
首先,我们使用现代构建工具创建项目基础结构:
# 创建项目目录
mkdir my-react-components
cd my-react-components
# 初始化monorepo结构
npm init -y
# 安装必要依赖
npm install -D typescript @types/react @types/react-dom
npm install -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs
npm install -D @rollup/plugin-typescript rollup-plugin-postcss
npm install -D rollup-plugin-terser rollup-plugin-peer-deps-external
1.2 项目结构设计
my-react-components/
├── packages/
│ ├── components/ # 组件源码
│ │ ├── src/
│ │ │ ├── Button/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.types.ts
│ │ │ │ ├── Button.test.tsx
│ │ │ │ └── Button.stories.tsx
│ │ │ └── index.ts # 组件统一导出
│ │ └── package.json
│ └── docs/ # 文档站点
├── package.json
├── tsconfig.json
├── rollup.config.js
└── .eslintrc.js
二、核心组件开发实践
2.1 基础Button组件实现
让我们从最基础的Button组件开始,展示如何构建一个健壮的React组件:
// packages/components/src/Button/Button.tsx
import React, { forwardRef, useMemo } from 'react';
import classNames from 'classnames';
import { ButtonProps, ButtonSize, ButtonVariant } from './Button.types';
import './Button.css';
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
fullWidth = false,
className,
onClick,
...restProps
},
ref
) => {
// 使用useMemo优化样式计算
const buttonClasses = useMemo(() => {
return classNames(
'btn',
`btn-${variant}`,
`btn-${size}`,
{
'btn-disabled': disabled,
'btn-loading': loading,
'btn-full-width': fullWidth,
},
className
);
}, [variant, size, disabled, loading, fullWidth, className]);
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (disabled || loading) return;
onClick?.(e);
};
return (
<button
ref={ref}
className={buttonClasses}
disabled={disabled || loading}
onClick={handleClick}
aria-busy={loading}
{...restProps}
>
{loading && (
<span className="btn-spinner" aria-hidden="true">
<svg className="spinner" viewBox="0 0 50 50">
<circle className="path" cx="25" cy="25" r="20" fill="none" />
</svg>
</span>
)}
<span className="btn-content">{children}</span>
</button>
);
}
);
Button.displayName = 'Button';
export default Button;
2.2 TypeScript类型定义
// packages/components/src/Button/Button.types.ts
export type ButtonSize = 'small' | 'medium' | 'large';
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
export interface ButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
/** 按钮变体 */
variant?: ButtonVariant;
/** 按钮尺寸 */
size?: ButtonSize;
/** 是否禁用 */
disabled?: boolean;
/** 加载状态 */
loading?: boolean;
/** 是否撑满容器宽度 */
fullWidth?: boolean;
/** 点击事件 */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** 自定义类名 */
className?: string;
/** 子元素 */
children?: React.ReactNode;
}
2.3 样式系统设计
/* packages/components/src/Button/Button.css */
.btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid transparent;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
outline: none;
}
/* 尺寸系统 */
.btn-small {
padding: 6px 12px;
font-size: 12px;
line-height: 1.5;
}
.btn-medium {
padding: 8px 16px;
font-size: 14px;
line-height: 1.5;
}
.btn-large {
padding: 12px 24px;
font-size: 16px;
line-height: 1.6;
}
/* 变体系统 */
.btn-primary {
background-color: #1890ff;
color: white;
}
.btn-primary:hover:not(.btn-disabled) {
background-color: #40a9ff;
}
.btn-primary:active:not(.btn-disabled) {
background-color: #096dd9;
}
.btn-secondary {
background-color: #f5f5f5;
color: #333;
border-color: #d9d9d9;
}
/* 状态系统 */
.btn-disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-loading {
cursor: wait;
}
.btn-full-width {
width: 100%;
}
/* 加载动画 */
.btn-spinner {
display: inline-flex;
margin-right: 8px;
}
.spinner {
animation: rotate 1.4s linear infinite;
width: 16px;
height: 16px;
}
.spinner .path {
stroke: currentColor;
stroke-width: 4;
stroke-linecap: round;
animation: dash 1.4s ease-in-out infinite;
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
三、构建与打包优化
3.1 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 peerDepsExternal from 'rollup-plugin-peer-deps-external';
import { terser } from 'rollup-plugin-terser';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
const packageJson = require('./package.json');
export default {
input: 'packages/components/src/index.ts',
output: [
{
file: packageJson.main,
format: 'cjs',
sourcemap: true,
exports: 'named',
},
{
file: packageJson.module,
format: 'esm',
sourcemap: true,
},
{
file: packageJson.unpkg,
format: 'umd',
name: 'MyComponents',
sourcemap: true,
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
],
plugins: [
peerDepsExternal(),
resolve({
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: 'dist',
exclude: ['**/*.test.tsx', '**/*.stories.tsx'],
}),