引言:为什么需要自建组件库?
在现代前端开发中,组件库已成为提升开发效率、保证产品一致性的关键工具。虽然市面上有Ant Design、Material-UI等优秀的开源组件库,但在实际项目中,我们常常会遇到以下痛点:
- 样式定制困难:现有组件库的样式体系与品牌设计语言不匹配
- 性能开销大:引入完整组件库导致包体积膨胀
- 功能冗余:80%的功能用不到,却要为20%的核心功能买单
- 维护成本高:第三方库的版本升级可能带来破坏性变更
本文将带你从零开始,构建一个高性能、可维护、易扩展的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