从零到一:现代化 React 组件库搭建指南
基于 TypeScript + PropTypes 双重校验的 React 组件库,采用 Monorepo 架构,配合 ESLint + Prettier 代码规范,使用 Rollup 进行打包构建。
🎯 项目架构
技术栈选型
- 开发语言: TypeScript + JavaScript
- UI 框架: React 18+
- 校验方案: TypeScript + PropTypes 双重校验
- 架构模式: Monorepo (多包管理)
- 包管理器: pnpm
- 构建工具: Rollup
- 代码规范: ESLint + Prettier
- 开发工具: Vite
项目结构
bash
组件库/
├── packages/
│ ├── components/ # 组件包
│ │ ├── src/
│ │ │ ├── Toast/ # Toast组件
│ │ │ ├── Message/ # Message组件
│ │ │ └── index.ts # 统一导出
│ │ ├── dist/ # 构建产物
│ │ └── package.json
│ ├── example/ # 示例应用
│ └── utils/ # 工具包
├── package.json # 根配置
├── pnpm-workspace.yaml # workspace配置
├── tsconfig.json # TS配置
├── rollup.config.js # 构建配置
├── .eslintrc.js # ESLint配置
└── .prettierrc # Prettier配置
🚀 项目初始化
1. 创建项目并初始化
bash
mkdir react-component-library && cd react-component-library
npm init -y
2. 安装核心依赖
bash
# 开发依赖
pnpm add -D typescript @types/react @types/react-dom
pnpm add -D rollup @rollup/plugin-typescript @rollup/plugin-node-resolve
pnpm add -D @rollup/plugin-commonjs @rollup/plugin-babel rollup-plugin-dts
pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks prettier
pnpm add -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
# 生产依赖
pnpm add prop-types
pnpm add -P react react-dom
⚙️ 核心配置文件
1. Monorepo 配置 (pnpm-workspace.yaml)
yaml
packages:
- 'packages/*'
2. TypeScript 配置 (tsconfig.json)
json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"strict": true,
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"declaration": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@my-ui/*": ["packages/*/src"]
}
},
"include": ["packages/*/src/**/*"],
"exclude": ["node_modules", "dist"]
}
3. Rollup 打包配置 (rollup.config.js)
javascript
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import dts from 'rollup-plugin-dts';
export default [
// JS构建
{
input: 'packages/components/src/index.ts',
external: ['react', 'react-dom', 'prop-types'],
output: [
{
file: 'packages/components/dist/index.esm.js',
format: 'esm',
sourcemap: true,
},
{
file: 'packages/components/dist/index.cjs.js',
format: 'cjs',
sourcemap: true,
exports: 'named',
},
],
plugins: [
nodeResolve({ extensions: ['.js', '.jsx', '.ts', '.tsx'] }),
commonjs(),
typescript({ declaration: false }),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
}),
],
},
// 类型声明构建
{
input: 'packages/components/src/index.ts',
external: ['react', 'react-dom', 'prop-types'],
output: { file: 'packages/components/dist/index.d.ts', format: 'esm' },
plugins: [dts()],
},
];
🎨 组件开发实践
1. 组件类型定义
typescript
// packages/components/src/Toast/types.ts
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface ToastOptions {
type?: ToastType;
duration?: number;
closable?: boolean;
onClose?: () => void;
}
export interface ToastProps extends ToastOptions {
message: string;
id: string;
visible: boolean;
onDestroy: (id: string) => void;
}
2. 组件实现(TypeScript + PropTypes 双重校验)
tsx
// packages/components/src/Toast/Toast.tsx
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { ToastProps, ToastType } from './types';
const ICONS: Record<ToastType, string> = {
success: '✓',
error: '✕',
warning: '⚠',
info: 'ⓘ',
};
const Toast: React.FC<ToastProps> = ({
id,
message,
type = 'info',
duration = 3000,
visible,
onClose,
onDestroy,
}) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (visible) {
setTimeout(() => setIsVisible(true), 50);
if (duration > 0) {
setTimeout(() => {
setIsVisible(false);
onClose?.();
setTimeout(() => onDestroy(id), 300);
}, duration);
}
}
}, [visible, duration]);
return (
<div className={`toast toast-${type} ${isVisible ? 'toast-visible' : ''}`}>
<span className={`toast-icon toast-icon-${type}`}>{ICONS[type]}</span>
<div className="toast-content">{message}</div>
<button onClick={() => setIsVisible(false)}>✕</button>
</div>
);
};
// PropTypes 运行时校验
Toast.propTypes = {
id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.oneOf(['success', 'error', 'warning', 'info']),
duration: PropTypes.number,
visible: PropTypes.bool.isRequired,
onDestroy: PropTypes.func.isRequired,
};
export default Toast;
3. 全局管理器
tsx
// packages/components/src/Toast/ToastManager.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import Toast from './Toast';
import { ToastOptions } from './types';
class ToastManager {
private container: HTMLElement;
private root: any;
private toasts = new Map();
constructor() {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
this.root = createRoot(this.container);
}
show(message: string, options: ToastOptions = {}) {
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const toast = { id, message, visible: true, ...options };
this.toasts.set(id, toast);
this.render();
return id;
}
success = (message: string, options?: Omit<ToastOptions, 'type'>) =>
this.show(message, { ...options, type: 'success' });
error = (message: string, options?: Omit<ToastOptions, 'type'>) =>
this.show(message, { ...options, type: 'error' });
private render() {
const elements = Array.from(this.toasts.values()).map((toast) => (
<Toast key={toast.id} {...toast} onDestroy={this.remove} />
));
this.root.render(<>{elements}</>);
}
private remove = (id: string) => {
this.toasts.delete(id);
this.render();
};
}
export const toast = new ToastManager();
4. 统一导出
typescript
// packages/components/src/index.ts
export { default as Toast, toast } from './Toast';
export type { ToastType, ToastOptions, ToastProps } from './Toast';
export const version = '1.0.0';
📦 构建与发布
1. 构建脚本配置
json
{
"scripts": {
"build": "rollup -c",
"build:watch": "rollup -c -w",
"lint": "eslint packages/ --ext .ts,.tsx",
"format": "prettier --write packages/**/*.{ts,tsx}",
"dev": "pnpm --filter example dev"
}
}
2. 组件包配置
json
// packages/components/package.json
{
"name": "your-react-design",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
}
3. 发布流程
bash
# 1. 构建项目
pnpm build
# 2. 更新版本
cd packages/components && npm version patch
# 3. 发布到npm
npm publish
🛠️ 开发体验优化
1. 示例应用配置
typescript
// packages/example/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@my-ui/components': resolve(__dirname, '../components/src'),
},
},
});
2. 代码规范
javascript
// .eslintrc.js
module.exports = {
extends: ['eslint:recommended', 'plugin:react/recommended'],
parser: '@typescript-eslint/parser',
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'warn',
},
};
🎉 总结
这个组件库的核心特点:
- 双重校验: TypeScript 编译时 + PropTypes 运行时
- 现代化工具链: pnpm + Rollup + ESLint + Prettier
- Monorepo 架构: 多包管理,便于维护
- 完整的开发体验: 热更新、类型提示、代码规范
- 可扩展性: 易于添加新组件和功能
通过这套架构,可以快速搭建一个现代化、类型安全、开发体验良好的 React 组件库。