组件库方案
从零构建企业级 React/Vue 组件库的完整指南
目录
架构设计
Monorepo 结构
packages/
├── components/ # 组件源码
│ ├── src/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ ├── Button.module.css
│ │ │ └── index.ts
│ │ ├── Input/
│ │ └── index.ts # 统一导出
│ ├── package.json
│ └── tsconfig.json
├── icons/ # 图标库
├── hooks/ # 通用 Hooks
├── utils/ # 工具函数
└── docs/ # 文档站点组件结构规范
typescript
// Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';
// Button/Button.tsx
import { forwardRef } from 'react';
import styles from './Button.module.css';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', loading, children, className, ...props }, ref) => {
return (
<button
ref={ref}
className={`${styles.button} ${styles[variant]} ${styles[size]} ${className || ''}`}
disabled={loading || props.disabled}
{...props}
>
{loading && <span className={styles.spinner} />}
{children}
</button>
);
}
);
Button.displayName = 'Button';构建配置
多格式输出
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
export default defineConfig({
plugins: [
react(),
dts({ include: ['src'] }) // 生成 .d.ts
],
build: {
lib: {
entry: 'src/index.ts',
name: 'MyUI',
formats: ['es', 'cjs', 'umd'],
fileName: (format) => `my-ui.${format}.js`
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
}
}
});package.json 配置
json
{
"name": "@myorg/ui",
"version": "1.0.0",
"type": "module",
"main": "./dist/my-ui.cjs.js",
"module": "./dist/my-ui.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/my-ui.es.js",
"require": "./dist/my-ui.cjs.js",
"types": "./dist/index.d.ts"
},
"./styles": "./dist/style.css"
},
"files": ["dist"],
"sideEffects": ["**/*.css"],
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
}Tree-shaking 支持
typescript
// src/index.ts - 具名导出支持 Tree-shaking
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
// ❌ 避免 barrel 文件中的副作用
// export * from './Button'; // 可能妨碍 Tree-shaking样式方案
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| CSS Modules | 作用域隔离、普通 CSS 语法 | 需要配置、类名动态 |
| CSS-in-JS | 类型安全、动态样式 | 运行时开销、SSR 复杂 |
| Tailwind | 原子化、按需生成 | 类名冗长、预设约束 |
| vanilla-extract | 零运行时、类型安全 | 学习成本 |
CSS 变量主题
css
/* theme.css */
:root, [data-theme="light"] {
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-bg: #ffffff;
--color-text: #1a1a1a;
--radius-md: 8px;
--spacing-md: 16px;
}
[data-theme="dark"] {
--color-primary: #60a5fa;
--color-primary-hover: #3b82f6;
--color-bg: #0f172a;
--color-text: #f1f5f9;
}css
/* Button.module.css */
.button {
background: var(--color-primary);
color: white;
padding: var(--spacing-md);
border-radius: var(--radius-md);
}
.button:hover {
background: var(--color-primary-hover);
}文档系统
Storybook 配置
typescript
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y', // 无障碍检查
'@storybook/addon-interactions' // 交互测试
],
framework: '@storybook/react-vite'
};
export default config;Story 编写
tsx
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline']
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg']
}
}
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
children: 'Primary Button',
variant: 'primary'
}
};
export const Loading: Story = {
args: {
children: 'Loading...',
loading: true
}
};发布流程
Changesets 版本管理
bash
# 安装
pnpm add -D @changesets/cli
# 初始化
npx changeset init
# 添加变更记录
npx changeset
# 更新版本号
npx changeset version
# 发布
npx changeset publishCHANGELOG 示例
markdown
# @myorg/ui
## 1.2.0
### Minor Changes
- 新增 `Tooltip` 组件
- `Button` 组件支持 `loading` 状态
### Patch Changes
- 修复 `Modal` 关闭动画问题CI/CD 配置
yaml
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- run: pnpm install
- run: pnpm build
- run: pnpm test
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
publish: pnpm changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}按需导入
Babel 插件方式
javascript
// babel.config.js
module.exports = {
plugins: [
['import', {
libraryName: '@myorg/ui',
libraryDirectory: 'es',
style: true // 自动引入样式
}]
]
};现代方案:package.json exports
json
{
"exports": {
".": "./dist/index.js",
"./Button": {
"import": "./dist/Button/index.js",
"types": "./dist/Button/index.d.ts"
},
"./Input": {
"import": "./dist/Input/index.js",
"types": "./dist/Input/index.d.ts"
}
}
}typescript
// 使用方直接具名导入即可 Tree-shaking
import { Button } from '@myorg/ui';高频面试题
Q1: 组件库如何支持 Tree-shaking?
- ESM 格式输出:使用
"type": "module"和 ES 模块语法 - 避免副作用:
package.json声明"sideEffects": false或列出有副作用的文件 - 具名导出:避免
export default,使用export { Component } - 独立入口:
exports字段提供组件级入口
Q2: 组件库样式方案如何选择?
| 场景 | 推荐方案 |
|---|---|
| 需要主题定制 | CSS 变量 + CSS Modules |
| 追求类型安全 | vanilla-extract |
| 已有 Tailwind 生态 | Tailwind + cva |
| 动态样式多 | CSS-in-JS (styled-components) |
Q3: 如何设计组件 API?
- 受控/非受控统一:同时支持
value和defaultValue - 组合优于继承:使用 Compound Components 模式
- 转发 ref:始终使用
forwardRef - 扩展原生属性:继承
HTMLAttributes - 语义化 Props:
variant优于type
Q4: 组件库如何保证质量?
- 单元测试:React Testing Library
- 视觉回归:Chromatic + Storybook
- 无障碍检测:@storybook/addon-a11y
- 类型检查:TypeScript strict mode
- Lint 规则:ESLint + Prettier
工具推荐
| 类别 | 工具 |
|---|---|
| 构建 | Vite / tsup / unbuild |
| Monorepo | pnpm workspace / Turborepo |
| 文档 | Storybook / Docusaurus |
| 版本 | Changesets |
| 测试 | Vitest / Playwright |
| 样式 | vanilla-extract / Tailwind |