Skip to content

组件库开发实战

📦 从零到一构建企业级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复杂,学习成本高
  • 完全专用:组件易用但复用性差,维护成本高
  • 分层设计:基础组件通用,业务组件专用

我的选择分层组件架构

typescript
// 分层组件架构设计
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设计系统

typescript
// 统一的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:主题化与定制化

问题:如何支持多品牌、多主题的需求?

我的解决方案设计令牌 + 主题系统

typescript
// 设计令牌系统
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架构来管理组件库项目:

typescript
// 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'
  }
}

🛠️ 构建系统设计

typescript
// 构建系统配置
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() // 分析打包结果
  ]
};

📋 包管理策略

typescript
// 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. 组件开发最佳实践

🧩 组件开发模板

typescript
// 标准组件开发模板
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;

🧪 组件测试策略

typescript
// 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集成

typescript
// 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. **经验沉淀**:总结最佳实践和方法论

### 📝 最终建议

组件库不仅仅是技术项目,更是**团队协作和设计标准化的载体**。成功的组件库能够:

- **提升开发效率**:减少重复工作,提高开发速度
- **保证设计一致性**:统一用户体验,强化品牌形象
- **降低维护成本**:集中维护,减少分散的技术债务
- **促进团队协作**:建立共同的技术语言和标准

记住:**优秀的组件库是进化出来的,不是一蹴而就的**。从小做起,持续改进,以用户价值为导向,才能建设出真正有价值的组件库。

---

*组件库的成功在于平衡通用性与易用性,在于将设计系统转化为开发者工具。希望这些实战经验能够帮助你建设出色的组件库,提升团队的开发效率和产品质量。*

React 技术栈最佳实践学习和总结 | 10年前端React生涯的大总结