组件库开发实战
📦 从零到一构建企业级React组件库的完整实践指南
🔍 问题背景:为什么需要组件库?
💔 多项目开发的痛点
作为一个曾经负责过5个产品线、20+项目的前端架构师,我深刻体会到没有统一组件库时的痛苦:
📊 真实的代码重复统计
让我用一个真实的企业案例来说明没有组件库时的痛苦。这家公司有5个产品线,20多个项目,当我加入时正面临严重的代码重复和一致性问题。
项目背景:多产品线SaaS公司
- 公司规模:300人,前端团队40人
- 产品线:CRM、ERP、财务系统、人事系统、项目管理
- 技术栈:React + TypeScript
- 问题现状:各个产品独立开发,没有统一的组件标准
代码重复的惊人数据
| 组件类型 | 重复实现数量 | 总代码行数 | 主要问题 |
|---|---|---|---|
| 按钮组件 | 15种不同实现 | 2400行重复代码 | 每次设计变更需要修改15个地方 |
| 模态框组件 | 8种不同实现 | 3200行重复代码 | 12个不同的bug分散在各个实现中 |
| 表单组件 | 20种不同实现 | 8000行重复代码 | 25个一致性问题 |
| 表格组件 | 12种不同实现 | 6000行重复代码 | 性能差异巨大 |
| 导航组件 | 6种不同实现 | 4000行重复代码 | 用户体验不一致 |
业务影响的量化分析
这种代码重复和不一致带来了严重的业务后果:
开发效率下降40%: 新功能开发时,开发者需要先研究每个项目的组件实现方式,然后选择或重新实现。一个简单的表单页面,在有组件库的情况下2天就能完成,但在这种环境下需要3-4天。
维护成本增加60%: 当设计系统发生变更时(比如品牌颜色调整),需要在所有项目中分别修改。一次看似简单的颜色调整,实际需要修改100多个文件,耗时2-3周。
Bug率增加30%: 由于每个项目都有自己的组件实现,相同的bug会在多个地方重复出现。更糟糕的是,一个项目修复了bug,其他项目可能仍然存在同样的问题。
设计一致性严重缺失: 虽然设计师提供了统一的设计规范,但由于没有强制执行机制,每个开发团队都有自己的理解和实现,导致用户在不同产品间切换时体验差异很大。
🤯 设计师的噩梦现场
还记得我们的设计师拿着设计稿对比5个产品的实际效果时的表情:
设计稿中的按钮:优雅的圆角、统一的阴影、一致的颜色
产品A的按钮:圆角8px、无阴影、颜色#1976d2
产品B的按钮:圆角4px、2px阴影、颜色#2196f3
产品C的按钮:圆角0px、1px边框、颜色#1565c0
产品D的按钮:圆角12px、4px阴影、颜色#0d47a1
产品E的按钮:圆角6px、无阴影、颜色#1e88e5设计师: "我设计了一套,你们实现了五套!"
🔥 开发团队的协作困境
在这种混乱的环境中,团队协作面临着前所未有的挑战,每个问题都在消耗着团队的效率和士气:
新人入职的漫长适应期
当新开发者加入团队时,他们不仅要学习业务逻辑和技术栈,还要熟悉每个项目独特的组件实现方式。我曾经见过一个有经验的React开发者,因为不熟悉我们项目中某个表单组件的特殊用法,花了整整一天时间调试一个本来很简单的功能。
每个新人平均需要2周时间才能适应不同项目的组件差异,这个时间本来可以用于更有价值的业务开发。更令人沮丧的是,即使是经验丰富的开发者,在切换项目时也需要重新熟悉组件的使用方式。
无效的代码审查循环
最让我头疼的是代码审查过程。每个团队都在重复审查相似的组件代码,每周我们团队花费约20小时在审查那些本质上相同的组件实现上。
更糟糕的是,由于缺乏统一的代码质量标准,不同审查者对同样的代码可能有完全不同的意见。Button组件的props设计、事件处理方式、样式管理方法,每个项目都有自己的"最佳实践",导致代码审查变成了无休止的争论。
Bug修复的噩梦
当我们发现某个组件存在bug时,噩梦就开始了。比如我们发现Modal组件在Safari浏览器上存在z-index问题,这个bug需要在8个不同的实现中分别修复。
修复过程不仅耗时(修复时间×项目数量),还存在巨大的遗漏风险。曾经有一次,我们以为修复了所有项目中的一个安全漏洞,结果在三个月后的安全审计中发现还有两个项目被遗漏了,差点造成严重的安全事故。
团队间协作的壁垒
各个产品团队就像孤岛一样独立工作,即使面临相似的技术挑战,也无法有效互相支援。CRM团队开发了一个优秀的数据表格组件,但ERP团队却不知道这个组件的存在,继续花费时间重新实现类似的功能。
这种分割不仅浪费了开发资源,还错失了知识共享和技术积累的机会。每个团队都在重复造轮子,而公司整体的技术能力却没有得到有效的积累和提升。
💡 组件库的价值思考
经过多次痛苦的经历,我总结出组件库的核心价值:
1. 一致性价值:从混乱到秩序的转变
组件库最直接的价值就是建立一致性,这种一致性是多层面的,从视觉设计到代码实现,都能带来质的改变。
设计一致性的革命性改善
在建立组件库之前,我们有5套完全不同的设计实现,每个产品都有自己的"设计语言"。用户在不同产品间切换时,感觉像是在使用完全不同公司的产品。
建立组件库后,我们只有1套标准设计系统,所有产品都遵循相同的设计原则。这种变化的影响是深远的:
用户体验变得连贯:用户在CRM系统中学会的交互方式,在ERP系统中同样适用。客户反馈说现在使用我们的产品套件"感觉像是一个整体",这直接提升了客户满意度和产品粘性。
品牌形象得到统一:一致的视觉风格让我们的产品系列具有了强烈的品牌识别度。销售团队反馈说,客户现在能够一眼认出我们的产品界面,这在竞标过程中成为了一个加分项。
代码一致性的深层价值
更重要的变化发生在代码层面。从20种不同的Button实现方式,到1个标准Button组件,这种统一带来的价值远超表面的代码减少:
认知负担大幅降低:开发者不再需要在每个项目中重新学习组件的使用方式。一个开发者掌握了组件库,就能够在所有项目中高效工作。
维护成本指数级降低:以前一个Button的样式调整需要修改20个文件,现在只需要修改1个组件,其他所有地方自动更新。这种维护效率的提升是指数级的。
代码质量标准得到统一:组件库建立了事实上的代码质量标准。所有开发者都能看到"优秀的React组件应该是什么样子",这对整个团队的代码能力提升起到了重要作用。
行为一致性的深层价值:各项目的交互行为得到统一,用户在不同产品间切换时有一致的操作体验,大大降低了学习成本。
2. 效率价值:开发效率的革命性提升
组件库对开发效率的提升是全方位的,从单个组件的开发到整个团队的协作都有显著改善:
开发速度的巨大飞跃:以前每个组件平均需要2天的开发时间(包括设计、编码、测试、调试),现在直接使用现成组件,只需要10分钟的配置时间,开发效率提升了95%。
维护效率的指数级改善:一个bug以前需要在N个项目中分别修复,现在修复一次就能让所有项目受益,维护效率提升了N倍。随着项目数量的增长,这种效率优势会越来越明显。
团队协作的质的飞跃:以前团队间无法有效共享组件开发经验,现在有了统一的组件文档和最佳实践,新人培训、知识传递、跨团队协作的效率都显著提升。
3. 质量价值:从参差不齐到统一优秀
组件库不仅提升了效率,更重要的是建立了质量标准: codeQuality: { testing: '组件库有完整的测试覆盖', accessibility: '无障碍功能在组件级别保证', performance: '性能优化在组件级别实现', security: '安全性在组件级别验证' };
designQuality: { designSystem: '基于成熟的设计系统', userExperience: '经过用户体验验证', responsive: '响应式设计在组件级别实现', themeable: '主题化支持品牌定制' }; }
🧠 组件库架构设计思考
🎯 核心设计挑战
在设计组件库时,我面临几个关键的架构决策:
挑战1:通用性 vs 专用性
问题:如何平衡组件的通用性和特定业务场景的需求?
思考过程:
- 完全通用:组件功能强大但API复杂,学习成本高
- 完全专用:组件易用但复用性差,维护成本高
- 分层设计:基础组件通用,业务组件专用
我的选择:分层组件架构
// 分层组件架构设计
interface LayeredComponentArchitecture {
// 1. 原子组件层 (Atomic Components)
atomic: {
purpose: '最基础的UI元素',
characteristics: ['高度通用', '无业务逻辑', '高度可定制'],
examples: ['Button', 'Input', 'Icon', 'Text'],
principles: [
'单一职责:只做一件事',
'无状态:不包含内部状态',
'可组合:可以组合成复杂组件'
]
};
// 2. 分子组件层 (Molecular Components)
molecular: {
purpose: '由原子组件组合而成的功能单元',
characteristics: ['中等通用性', '轻量业务逻辑', '场景化'],
examples: ['SearchBox', 'Pagination', 'DateRangePicker'],
principles: [
'组合优于继承',
'职责清晰',
'配置灵活'
]
};
// 3. 有机体组件层 (Organism Components)
organism: {
purpose: '复杂的功能模块',
characteristics: ['业务相关', '完整功能', '高度集成'],
examples: ['DataTable', 'Form', 'Header', 'Sidebar'],
principles: [
'功能完整',
'高度可配置',
'业务语义化'
]
};
// 4. 模板组件层 (Template Components)
template: {
purpose: '页面级布局和模板',
characteristics: ['页面级', '布局相关', '业务特定'],
examples: ['PageLayout', 'ListPageTemplate', 'FormPageTemplate'],
principles: [
'布局标准化',
'响应式设计',
'可扩展性'
]
};
}挑战2:API设计的一致性
问题:如何设计一致且直观的组件API?
思考过程:
- 命名规范:props命名要有一致的规律
- 行为一致:相似功能的组件行为要一致
- 扩展性:API要为未来的功能扩展留出空间
我的解决方案:API设计系统
// 统一的API设计规范
interface ComponentAPIDesignSystem {
// 1. 命名规范
namingConventions: {
// 布尔属性使用 is/has/can 前缀
booleanProps: ['isLoading', 'hasError', 'canEdit', 'disabled', 'readOnly'];
// 事件处理器使用 on 前缀
eventHandlers: ['onClick', 'onChange', 'onSubmit', 'onFocus', 'onBlur'];
// 渲染函数使用 render 前缀
renderProps: ['renderItem', 'renderHeader', 'renderFooter', 'renderEmpty'];
// 配置对象使用名词
configProps: ['options', 'config', 'settings', 'theme'];
};
// 2. 属性分类
propCategories: {
// 基础属性:每个组件都应该有的
base: ['className', 'style', 'children', 'id', 'testId'];
// 状态属性:组件状态相关
state: ['loading', 'disabled', 'error', 'value', 'defaultValue'];
// 行为属性:组件行为控制
behavior: ['size', 'variant', 'color', 'placement', 'trigger'];
// 事件属性:用户交互
events: ['onClick', 'onChange', 'onSubmit', 'onCancel'];
// 定制属性:外观和功能定制
customization: ['theme', 'icon', 'prefix', 'suffix', 'extra'];
};
// 3. 默认值策略
defaultValues: {
principle: '合理的默认值,减少必须的props',
examples: {
size: 'medium', // 中等尺寸作为默认
variant: 'default', // 默认样式变体
color: 'primary', // 主色调作为默认
placement: 'bottom', // 合理的默认位置
}
};
}
// 具体应用示例:Button组件的API设计
interface ButtonProps {
// 基础属性
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
id?: string;
testId?: string;
// 状态属性
loading?: boolean;
disabled?: boolean;
// 行为属性
size?: 'small' | 'medium' | 'large';
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
color?: 'primary' | 'success' | 'warning' | 'danger';
shape?: 'default' | 'round' | 'circle';
block?: boolean; // 是否占满宽度
// 事件属性
onClick?: (event: React.MouseEvent) => void;
onFocus?: (event: React.FocusEvent) => void;
onBlur?: (event: React.FocusEvent) => void;
// 定制属性
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
htmlType?: 'button' | 'submit' | 'reset';
}挑战3:主题化与定制化
问题:如何支持多品牌、多主题的需求?
我的解决方案:设计令牌 + 主题系统
// 设计令牌系统
interface DesignTokenSystem {
// 1. 基础令牌 (Primitive Tokens)
primitive: {
colors: {
blue50: '#f0f9ff',
blue100: '#e0f2fe',
blue500: '#3b82f6',
blue900: '#1e3a8a',
// ... 更多颜色
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
typography: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
fontSize: {
xs: '12px',
sm: '14px',
base: '16px',
lg: '18px',
xl: '20px',
}
}
};
// 2. 语义令牌 (Semantic Tokens)
semantic: {
colors: {
primary: 'var(--color-blue-500)',
primaryHover: 'var(--color-blue-600)',
success: 'var(--color-green-500)',
warning: 'var(--color-yellow-500)',
danger: 'var(--color-red-500)',
text: 'var(--color-gray-900)',
textSecondary: 'var(--color-gray-600)',
background: 'var(--color-white)',
border: 'var(--color-gray-200)',
},
spacing: {
componentPadding: 'var(--spacing-md)',
componentMargin: 'var(--spacing-lg)',
sectionGap: 'var(--spacing-xl)',
}
};
// 3. 组件令牌 (Component Tokens)
component: {
button: {
height: {
small: '32px',
medium: '40px',
large: '48px',
},
padding: {
small: '0 var(--spacing-sm)',
medium: '0 var(--spacing-md)',
large: '0 var(--spacing-lg)',
},
borderRadius: {
default: '6px',
round: '20px',
circle: '50%',
}
},
input: {
height: {
small: '32px',
medium: '40px',
large: '48px',
},
fontSize: {
small: 'var(--fontSize-sm)',
medium: 'var(--fontSize-base)',
large: 'var(--fontSize-lg)',
}
}
};
}
// 主题系统实现
class ThemeSystem {
private themes: Map<string, Theme> = new Map();
private currentTheme: string = 'default';
// 注册主题
registerTheme(name: string, theme: Theme) {
this.themes.set(name, theme);
}
// 应用主题
applyTheme(themeName: string) {
const theme = this.themes.get(themeName);
if (!theme) {
console.warn(`Theme ${themeName} not found`);
return;
}
this.currentTheme = themeName;
// 将主题令牌应用到CSS变量
const root = document.documentElement;
Object.entries(theme.tokens).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
// 触发主题变更事件
window.dispatchEvent(new CustomEvent('themeChange', {
detail: { theme: themeName }
}));
}
// 获取当前主题
getCurrentTheme(): Theme {
return this.themes.get(this.currentTheme)!;
}
// 创建主题变体
createThemeVariant(baseName: string, variantName: string, overrides: Partial<Theme>) {
const baseTheme = this.themes.get(baseName);
if (!baseTheme) {
throw new Error(`Base theme ${baseName} not found`);
}
const variantTheme = this.mergeThemes(baseTheme, overrides);
this.registerTheme(variantName, variantTheme);
}
private mergeThemes(base: Theme, overrides: Partial<Theme>): Theme {
return {
...base,
tokens: { ...base.tokens, ...overrides.tokens },
components: { ...base.components, ...overrides.components },
};
}
}
// 使用示例
const themeSystem = new ThemeSystem();
// 注册默认主题
themeSystem.registerTheme('default', {
tokens: {
'color-primary': '#3b82f6',
'color-success': '#10b981',
'spacing-md': '16px',
// ... 更多令牌
},
components: {
button: {
primaryBackground: 'var(--color-primary)',
primaryHover: 'var(--color-blue-600)',
}
}
});
// 创建暗色主题
themeSystem.createThemeVariant('default', 'dark', {
tokens: {
'color-text': '#ffffff',
'color-background': '#1f2937',
'color-border': '#374151',
}
});💡 核心实施方案
🎯 1. 项目架构设计
📦 Monorepo架构
基于我的实践经验,我强烈推荐使用Monorepo架构来管理组件库项目:
// Monorepo项目结构
interface MonorepoStructure {
'packages/': {
'core/': {
purpose: '核心组件库',
exports: ['Button', 'Input', 'Modal', '...'],
dependencies: ['React', 'clsx', 'framer-motion']
},
'icons/': {
purpose: '图标库',
exports: ['CheckIcon', 'CloseIcon', 'ArrowIcon', '...'],
dependencies: ['React', 'SVG icons']
},
'themes/': {
purpose: '主题包',
exports: ['defaultTheme', 'darkTheme', 'brandTheme'],
dependencies: ['Design tokens']
},
'utils/': {
purpose: '工具函数',
exports: ['classNames', 'formatters', 'validators'],
dependencies: ['Lodash-es', 'date-fns']
},
'playground/': {
purpose: '开发和测试环境',
dependencies: ['@company/ui-core', 'Storybook', 'React']
},
'docs/': {
purpose: '文档站点',
dependencies: ['VitePress', '@company/ui-core']
}
},
'tools/': {
'build/': 'Build scripts and configurations',
'eslint-config/': 'Shared ESLint configuration',
'typescript-config/': 'Shared TypeScript configuration'
},
'apps/': {
'example-app/': 'Example applications using the component library'
}
}🛠️ 构建系统设计
// 构建系统配置
interface BuildSystemConfig {
// 主构建工具
bundler: {
tool: 'Rollup',
reason: '专为库构建优化,Tree-shaking支持好',
alternatives: ['Webpack', 'Vite', 'Parcel']
};
// 多格式输出
output: {
esm: {
format: 'ES Modules',
target: './dist/esm/',
usage: '现代打包工具和Tree-shaking'
},
cjs: {
format: 'CommonJS',
target: './dist/cjs/',
usage: 'Node.js环境和旧版打包工具'
},
umd: {
format: 'UMD',
target: './dist/umd/',
usage: '浏览器直接引用和CDN'
},
types: {
format: 'TypeScript Declarations',
target: './dist/types/',
usage: 'TypeScript类型支持'
}
};
// 优化策略
optimization: {
treeShaking: '确保组件可以被Tree-shake',
bundleAnalysis: '分析包大小和依赖关系',
compression: '压缩和minify代码',
sourceMap: '生成Source Map便于调试'
};
}
// Rollup配置示例
const rollupConfig = {
input: 'src/index.ts',
external: ['react', 'react-dom'], // 不打包React
output: [
{
file: 'dist/esm/index.js',
format: 'esm',
sourcemap: true
},
{
file: 'dist/cjs/index.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/umd/index.js',
format: 'umd',
name: 'UILibrary',
globals: {
react: 'React',
'react-dom': 'ReactDOM'
},
sourcemap: true
}
],
plugins: [
typescript({
declaration: true,
declarationDir: 'dist/types',
rootDir: 'src'
}),
resolve(),
commonjs(),
terser(), // 压缩代码
bundleAnalyzer() // 分析打包结果
]
};📋 包管理策略
// package.json配置策略
interface PackageManagementStrategy {
// 主包配置
main: {
name: '@company/ui-core',
version: 'Semantic Versioning (1.0.0)',
main: './dist/cjs/index.js', // CommonJS入口
module: './dist/esm/index.js', // ES Module入口
types: './dist/types/index.d.ts', // TypeScript类型入口
sideEffects: false, // 标记为无副作用,支持Tree-shaking
files: ['dist/', 'README.md'], // 只发布必要文件
};
// 依赖管理
dependencies: {
strategy: 'Minimal dependencies',
peerDependencies: ['react', 'react-dom'], // 宿主项目提供
devDependencies: ['构建工具', '测试工具', '开发工具'],
optionalDependencies: ['可选的增强功能依赖']
};
// 发布配置
publishing: {
registry: 'npm or private registry',
access: 'public or restricted',
versioning: 'Semantic Versioning + Conventional Commits',
changelog: 'Automated changelog generation'
};
}🎯 2. 组件开发最佳实践
🧩 组件开发模板
// 标准组件开发模板
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { BaseComponentProps, ComponentSize, ComponentVariant } from '../types';
import { useTheme } from '../hooks/useTheme';
import styles from './Button.module.css';
// 1. 定义Props接口
export interface ButtonProps extends BaseComponentProps {
// 状态属性
loading?: boolean;
disabled?: boolean;
// 外观属性
size?: ComponentSize;
variant?: ComponentVariant;
color?: 'primary' | 'success' | 'warning' | 'danger';
shape?: 'default' | 'round' | 'circle';
block?: boolean;
// 功能属性
htmlType?: 'button' | 'submit' | 'reset';
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
// 事件属性
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
}
// 2. 组件实现
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
// 解构props并设置默认值
children,
className,
style,
loading = false,
disabled = false,
size = 'medium',
variant = 'primary',
color = 'primary',
shape = 'default',
block = false,
htmlType = 'button',
icon,
iconPosition = 'left',
onClick,
onFocus,
onBlur,
testId,
...restProps
}, ref) => {
// 3. Hooks和逻辑
const theme = useTheme();
// 计算最终的disabled状态
const isDisabled = disabled || loading;
// 4. 事件处理
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (isDisabled) return;
onClick?.(event);
};
// 5. 样式计算
const buttonClasses = clsx(
styles.button,
styles[`button--${size}`],
styles[`button--${variant}`],
styles[`button--${color}`],
styles[`button--${shape}`],
{
[styles['button--block']]: block,
[styles['button--loading']]: loading,
[styles['button--disabled']]: isDisabled,
[styles['button--icon-only']]: icon && !children,
},
className
);
// 6. 渲染逻辑
const renderIcon = () => {
if (loading) {
return <LoadingSpinner className={styles.spinner} />;
}
return icon;
};
const renderContent = () => {
if (!children && icon) {
return renderIcon();
}
return (
<>
{iconPosition === 'left' && renderIcon()}
{children && <span className={styles.content}>{children}</span>}
{iconPosition === 'right' && renderIcon()}
</>
);
};
// 7. 最终渲染
return (
<button
ref={ref}
type={htmlType}
className={buttonClasses}
style={style}
disabled={isDisabled}
onClick={handleClick}
onFocus={onFocus}
onBlur={onBlur}
data-testid={testId}
{...restProps}
>
{renderContent()}
</button>
);
});
// 8. 组件displayName
Button.displayName = 'Button';
// 9. 默认导出
export default Button;🧪 组件测试策略
// Button.test.tsx - 组件测试示例
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
// 测试工具函数
const renderButton = (props = {}) => {
return render(<Button {...props}>Click me</Button>);
};
describe('Button Component', () => {
// 1. 基础渲染测试
describe('Rendering', () => {
it('should render with children', () => {
renderButton();
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('should render with custom className', () => {
renderButton({ className: 'custom-class' });
expect(screen.getByRole('button')).toHaveClass('custom-class');
});
it('should render with testId', () => {
renderButton({ testId: 'my-button' });
expect(screen.getByTestId('my-button')).toBeInTheDocument();
});
});
// 2. Props测试
describe('Props', () => {
it('should apply size classes correctly', () => {
const { rerender } = renderButton({ size: 'small' });
expect(screen.getByRole('button')).toHaveClass('button--small');
rerender(<Button size="large">Click me</Button>);
expect(screen.getByRole('button')).toHaveClass('button--large');
});
it('should apply variant classes correctly', () => {
renderButton({ variant: 'secondary' });
expect(screen.getByRole('button')).toHaveClass('button--secondary');
});
it('should handle disabled state', () => {
renderButton({ disabled: true });
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(button).toHaveClass('button--disabled');
});
});
// 3. 交互测试
describe('Interactions', () => {
it('should call onClick when clicked', async () => {
const user = userEvent.setup();
const mockOnClick = jest.fn();
renderButton({ onClick: mockOnClick });
await user.click(screen.getByRole('button'));
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('should not call onClick when disabled', async () => {
const user = userEvent.setup();
const mockOnClick = jest.fn();
renderButton({ onClick: mockOnClick, disabled: true });
await user.click(screen.getByRole('button'));
expect(mockOnClick).not.toHaveBeenCalled();
});
it('should not call onClick when loading', async () => {
const user = userEvent.setup();
const mockOnClick = jest.fn();
renderButton({ onClick: mockOnClick, loading: true });
await user.click(screen.getByRole('button'));
expect(mockOnClick).not.toHaveBeenCalled();
});
});
// 4. 无障碍性测试
describe('Accessibility', () => {
it('should have proper focus management', async () => {
const user = userEvent.setup();
renderButton();
const button = screen.getByRole('button');
await user.tab();
expect(button).toHaveFocus();
});
it('should support keyboard navigation', async () => {
const user = userEvent.setup();
const mockOnClick = jest.fn();
renderButton({ onClick: mockOnClick });
const button = screen.getByRole('button');
button.focus();
await user.keyboard('{Enter}');
expect(mockOnClick).toHaveBeenCalledTimes(1);
await user.keyboard(' ');
expect(mockOnClick).toHaveBeenCalledTimes(2);
});
});
// 5. 边界情况测试
describe('Edge Cases', () => {
it('should handle icon-only buttons', () => {
const icon = <span data-testid="icon">👍</span>;
render(<Button icon={icon} />);
expect(screen.getByTestId('icon')).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveClass('button--icon-only');
});
it('should show loading spinner when loading', () => {
renderButton({ loading: true });
expect(screen.getByRole('button')).toHaveClass('button--loading');
// 可以进一步测试spinner的存在
});
});
// 6. 快照测试
describe('Snapshots', () => {
it('should match snapshot for default button', () => {
const { container } = renderButton();
expect(container.firstChild).toMatchSnapshot();
});
it('should match snapshot for all variants', () => {
const variants = ['primary', 'secondary', 'outline', 'ghost'] as const;
variants.forEach(variant => {
const { container } = renderButton({ variant });
expect(container.firstChild).toMatchSnapshot(`Button-${variant}`);
});
});
});
});🎯 3. 文档与开发体验
📚 Storybook集成
// Button.stories.tsx - Storybook故事
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { CheckIcon } from '@company/ui-icons';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
# Button组件
通用的按钮组件,支持多种样式、尺寸和状态。
## 何时使用
- 需要用户执行操作时
- 作为表单提交按钮
- 作为导航或功能触发器
## 设计原则
- **明确性**:按钮的作用应该显而易见
- **一致性**:相同类型的按钮应该有一致的样式
- **可访问性**:支持键盘导航和屏幕阅读器
`
}
}
},
argTypes: {
size: {
control: 'select',
options: ['small', 'medium', 'large'],
description: '按钮尺寸'
},
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost'],
description: '按钮样式变体'
},
color: {
control: 'select',
options: ['primary', 'success', 'warning', 'danger'],
description: '按钮颜色'
},
loading: {
control: 'boolean',
description: '加载状态'
},
disabled: {
control: 'boolean',
description: '禁用状态'
},
block: {
control: 'boolean',
description: '是否占满宽度'
}
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
// 基础故事
export const Default: Story = {
args: {
children: 'Button',
},
};
// 不同尺寸
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
<Button size="large">Large</Button>
</div>
),
parameters: {
docs: {
description: {
story: '按钮支持三种尺寸:small、medium(默认)、large'
}
}
}
};
// 不同变体
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
parameters: {
docs: {
description: {
story: '按钮支持四种样式变体,适用于不同的使用场景'
}
}
}
};
// 带图标
export const WithIcon: Story = {
render: () => (
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<Button icon={<CheckIcon />}>With Icon</Button>
<Button icon={<CheckIcon />} iconPosition="right">Icon Right</Button>
<Button icon={<CheckIcon />} />
</div>
),
parameters: {
docs: {
description: {
story: '按钮可以包含图标,支持左右位置和纯图标模式'
}
}
}
};
// 状态演示
export const States: Story = {
render: () => (
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
<Button>Normal</Button>
<Button loading>Loading</Button>
<Button disabled>Disabled</Button>
</div>
),
parameters: {
docs: {
description: {
story: '按钮的不同状态:正常、加载中、禁用'
}
}
}
};
// 交互测试
export const Interactive: Story = {
render: () => {
const [count, setCount] = React.useState(0);
return (
<div style={{ textAlign: 'center' }}>
<p>点击次数: {count}</p>
<Button onClick={() => setCount(c => c + 1)}>
点击我 (+1)
</Button>
</div>
);
},
parameters: {
docs: {
description: {
story: '交互式示例,展示按钮的点击事件处理'
}
}
}
};
// 可访问性演示
export const Accessibility: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<p>使用Tab键导航,Enter或空格键激活:</p>
<div style={{ display: 'flex', gap: '16px' }}>
<Button>First Button</Button>
<Button>Second Button</Button>
<Button disabled>Disabled Button</Button>
<Button>Last Button</Button>
</div>
</div>
),
parameters: {
docs: {
description: {
story: '演示按钮的键盘导航和无障碍功能'
}
}
}
};/* 基础样式 / .button { / 使用CSS变量支持主题 */ display: inline-flex; align-items: center; justify-content: center; border: none; cursor: pointer; font-family: var(--font-family-base); font-weight: var(--font-weight-medium); line-height: 1; text-decoration: none; user-select: none; white-space: nowrap; transition: all var(--transition-duration-base) var(--transition-timing-function-base);
/* 默认尺寸 */ height: var(--button-height-medium); padding: 0 var(--button-padding-medium); font-size: var(--button-font-size-medium); border-radius: var(--button-border-radius);
/* 聚焦样式 */ &:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } }
/* 尺寸变体 */ .button--small { height: var(--button-height-small); padding: 0 var(--button-padding-small); font-size: var(--button-font-size-small); }
.button--large { height: var(--button-height-large); padding: 0 var(--button-padding-large); font-size: var(--button-font-size-large); }
/* 样式变体 */ .button--primary { background-color: var(--color-primary); color: var(--color-white);
&:hover:not(:disabled) { background-color: var(--color-primary-hover); }
&:active:not(:disabled) { background-color: var(--color-primary-active); } }
.button--secondary { background-color: var(--color-gray-100); color: var(--color-gray-900);
&:hover:not(:disabled) { background-color: var(--color-gray-200); } }
.button--outline { background-color: transparent; color: var(--color-primary); border: 1px solid var(--color-primary);
&:hover:not(:disabled) { background-color: var(--color-primary); color: var(--color-white); } }
/* 形状变体 */ .button--round { border-radius: var(--button-height-medium); }
.button--circle { border-radius: 50%; width: var(--button-height-medium); padding: 0; }
/* 状态样式 */ .button--disabled { opacity: 0.6; cursor: not-allowed; }
.button--loading { cursor: not-allowed; }
.button--block { display: flex; width: 100%; }
/* 图标样式 */ .spinner { width: 1em; height: 1em; animation: spin 1s linear infinite; }
.content { margin: 0 0.25em; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
### 🎯 4. 发布与维护流程
#### 🚀 自动化发布流程
现代组件库需要建立完善的自动化发布流程,确保版本发布的可靠性和一致性。基于我在多个组件库项目中的实践:
**语义化版本管理:**
- **补丁版本(patch)**:修复bug,不影响API
- **次要版本(minor)**:新增功能,向后兼容
- **主要版本(major)**:破坏性变更,需要迁移指南
**发布流程自动化:**
通过GitHub Actions实现全自动化发布,包括代码检查、测试、构建、发布、文档更新等步骤。当代码合并到主分支时,自动检测变更类型并发布相应版本。
#### 📊 使用统计与反馈
建立组件使用数据收集机制,为迭代优化提供数据支撑:
**使用统计收集:**
- **组件使用频率**:哪些组件最常用,哪些很少用
- **API使用情况**:哪些属性经常使用,哪些可以简化
- **性能数据**:组件渲染性能,包体积影响
- **错误统计**:使用过程中的常见错误
**反馈收集渠道:**
- **GitHub Issues**:功能需求和bug反馈
- **内部调研**:定期的用户满意度调研
- **使用培训**:培训过程中收集使用难点
- **代码审查**:在代码审查中发现使用问题
#### 📈 渐进式推广策略
组件库的推广需要考虑团队接受度和学习成本:
**第一阶段:核心团队试点**
在核心团队中先行使用,收集反馈,快速迭代
**第二阶段:重点项目推广**
选择1-2个重点项目全面使用,建立最佳实践
**第三阶段:全面推广**
制定迁移计划,提供培训支持,逐步在所有项目中推广
### ⚠️ 常见陷阱与解决方案
#### 陷阱1:过度设计
**问题现象**:为了支持所有可能的用例,组件API变得过于复杂
**解决原则**:
- **80/20原则**:优先满足80%的常见需求
- **渐进增强**:先实现基础功能,再根据反馈扩展
- **可组合性**:通过组合解决复杂需求,而不是单个组件承担所有功能
#### 陷阱2:版本兼容性管理不当
**问题现象**:Breaking Changes处理不当,导致升级困难
#### 陷阱3:文档和实际实现不同步
**问题现象**:文档过时,开发者无法正确使用组件
**解决方案**:
### 📈 成功指标与持续改进
#### 🎯 关键指标监控
## 🎯 总结与建议
### 💡 核心成功要素
基于我多年的组件库建设经验,成功的关键要素是:
1. **业务驱动**:从实际业务需求出发,而不是为了技术而技术
2. **用户中心**:始终以开发者体验为中心设计API和工具
3. **质量优先**:建立完善的质量保证体系,确保组件库的可靠性
4. **渐进演进**:从小规模开始,根据反馈持续改进和扩展
5. **团队协作**:建立良好的协作机制和治理流程
### 🚀 实施路径建议
#### 📋 第一阶段:基础建设 (1-3个月)
1. **技术选型**:确定技术栈和工具链
2. **架构设计**:设计组件库的整体架构
3. **核心组件**:实现5-10个最常用的基础组件
4. **开发环境**:搭建开发、测试、文档环境
5. **团队培训**:培训核心开发团队
#### 🏗️ 第二阶段:扩展完善 (3-6个月)
1. **组件扩展**:根据业务需求扩展组件库
2. **主题系统**:实现完整的主题和定制化系统
3. **文档完善**:建立完整的文档和示例
4. **测试体系**:建立自动化测试和质量保证流程
5. **试点推广**:在部分项目中试点使用
#### 🚀 第三阶段:全面推广 (6-12个月)
1. **全面应用**:在所有新项目中使用组件库
2. **迁移计划**:制定旧项目的迁移计划和工具
3. **生态建设**:建立插件、工具和社区生态
4. **持续优化**:基于使用数据持续优化组件库
5. **经验沉淀**:总结最佳实践和方法论
### 📝 最终建议
组件库不仅仅是技术项目,更是**团队协作和设计标准化的载体**。成功的组件库能够:
- **提升开发效率**:减少重复工作,提高开发速度
- **保证设计一致性**:统一用户体验,强化品牌形象
- **降低维护成本**:集中维护,减少分散的技术债务
- **促进团队协作**:建立共同的技术语言和标准
记住:**优秀的组件库是进化出来的,不是一蹴而就的**。从小做起,持续改进,以用户价值为导向,才能建设出真正有价值的组件库。
---
*组件库的成功在于平衡通用性与易用性,在于将设计系统转化为开发者工具。希望这些实战经验能够帮助你建设出色的组件库,提升团队的开发效率和产品质量。*