在当今的前端开发中,组件化已成为构建复杂应用的核心范式。无论是大型企业应用还是中小型项目,拥有一个设计良好、可维护的组件库都能显著提升开发效率和代码质量。本文将带你从零开始,构建一个现代化的 React 组件库,涵盖架构设计、开发工具链、测试策略和发布流程等关键环节。
为什么需要自建组件库?
你可能会有疑问:市面上已经有 Ant Design、Material-UI 等优秀的组件库,为什么还要自建?原因包括:
- 品牌一致性:自定义设计语言,完全匹配产品视觉规范
- 性能优化:只包含项目需要的组件,避免不必要的代码
- 特殊业务需求:针对特定业务场景定制组件
- 技术栈控制:完全掌控技术选型和实现细节
项目初始化与架构设计
1. 选择正确的构建工具
现代组件库构建有多种选择,我们推荐使用 Vite + TypeScript 的组合:
# 创建项目
npm create vite@latest my-component-library -- --template react-ts
# 安装必要依赖
cd my-component-library
npm install -D @types/react @types/react-dom
npm install -D @vitejs/plugin-react @types/node
2. 配置 monorepo 结构
对于组件库,采用 monorepo 结构可以更好地管理多个包:
// package.json
{
"name": "my-component-library",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^1.10.0"
}
}
3. 设计组件目录结构
packages/
├── ui/
│ ├── src/
│ │ ├── components/
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.test.tsx
│ │ │ │ ├── Button.stories.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.module.css
│ │ │ └── index.ts
│ │ └── index.ts
├── docs/
│ └── ... # 文档网站
└── playground/
└── ... # 开发测试环境
核心组件开发实践
1. 基础 Button 组件实现
让我们从最基础的 Button 组件开始:
// packages/ui/src/components/Button/Button.tsx
import React, { forwardRef, ButtonHTMLAttributes } from 'react';
import classNames from 'classnames';
import styles from './styles.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;
/** 是否块级元素 */
block?: boolean;
/** 自定义类名 */
className?: string;
/** 点击事件 */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = 'primary',
size = 'medium',
loading = false,
disabled = false,
block = false,
className,
onClick,
...rest
},
ref
) => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (loading || disabled) {
event.preventDefault();
return;
}
onClick?.(event);
};
const buttonClasses = classNames(
styles.button,
styles[`button--${variant}`],
styles[`button--${size}`],
{
[styles['button--loading']]: loading,
[styles['button--disabled']]: disabled,
[styles['button--block']]: block,
},
className
);
return (
<button
ref={ref}
className={buttonClasses}
disabled={disabled || loading}
onClick={handleClick}
aria-busy={loading}
{...rest}
>
{loading && (
<span className={styles.loadingSpinner} aria-hidden="true" />
)}
<span className={styles.content}>{children}</span>
</button>
);
}
);
Button.displayName = 'Button';
2. 样式方案选择
我们推荐使用 CSS Modules + CSS Variables 的组合:
/* packages/ui/src/components/Button/styles.module.css */
.button {
--button-primary-bg: #1890ff;
--button-primary-hover: #40a9ff;
--button-primary-active: #096dd9;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
touch-action: manipulation;
outline: none;
}
.button--primary {
background-color: var(--button-primary-bg);
color: white;
}
.button--primary:hover:not(.button--disabled) {
background-color: var(--button-primary-hover);
}
.button--primary:active:not(.button--disabled) {
background-color: var(--button-primary-active);
}
.button--medium {
height: 32px;
padding: 4px 15px;
font-size: 14px;
}
.button--small {
height: 24px;
padding: 0 7px;
font-size: 12px;
}
.button--large {
height: 40px;
padding: 6px 19px;
font-size: 16px;
}
.button--block {
width: 100%;
}
.button--loading {
opacity: 0.65;
cursor: not-allowed;
}
.button--disabled {
opacity: 0.4;
cursor: not-allowed;
}
.loadingSpinner {
display: inline-block;
width: 14px;
height: 14px;
margin-right: 8px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: button-spin 1s linear infinite;
}
@keyframes button-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
3. 导出配置
// packages/ui/src/components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
// packages/ui/src/components/index.ts
export { Button } from './Button';
// packages/ui/src/index.ts
export * from './components';
开发工具链配置
1. TypeScript 配置
// packages/ui/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": 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"],
"references": [{ "path": "./tsconfig.node.json" }]
}
2. Vite 构建配置
// packages/ui/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,
include: ['src'],
exclude: ['**/*.