一、为什么需要自建组件库

在现在的前端开发中,我们经常会遇到这样的场景:公司内部有多个项目,每个项目都在重复开发类似的按钮、表单、弹窗等组件。这不仅浪费开发资源,还导致用户体验不一致。想象一下,如果电商平台的购物车按钮在PC端和移动端长得完全不一样,用户肯定会觉得困惑。

这时候,一个统一的企业级UI组件库就显得尤为重要了。它就像是前端开发的"乐高积木",让我们可以快速搭建出风格统一、功能完善的界面。而且,当设计规范更新时,我们只需要更新组件库,所有使用它的项目都会自动获得最新的样式和功能。

二、搭建组件库的基础准备

在开始之前,我们需要做好一些准备工作。首先确保你已经安装了Node.js(建议版本16+)和npm/yarn。这里我们选择使用React 18 + TypeScript + Storybook的技术栈,这是目前最流行的组合之一。

让我们先初始化项目:

# 使用create-react-app初始化项目
npx create-react-app my-ui-library --template typescript
cd my-ui-library

# 安装Storybook
npx storybook init

接下来,我们需要调整项目结构。一个好的组件库应该有这样的目录结构:

/src
  /components      # 所有组件
    /Button
      Button.tsx   # 组件实现
      Button.css   # 组件样式
      Button.test.tsx # 测试文件
      index.ts     # 导出组件
  /styles          # 全局样式
  /utils           # 工具函数

三、开发第一个组件:按钮

让我们从最基础的Button组件开始。在React中,一个好的组件应该具备以下特点:

  1. 清晰的props定义
  2. 完善的类型提示
  3. 良好的可扩展性
  4. 完整的样式隔离
// src/components/Button/Button.tsx
import React from 'react';
import './Button.css';

// 定义按钮类型
type ButtonType = 'primary' | 'default' | 'dashed' | 'text' | 'link';

// 定义按钮大小
type ButtonSize = 'large' | 'middle' | 'small';

// 定义Button组件的Props接口
interface ButtonProps {
  type?: ButtonType;      // 按钮类型
  size?: ButtonSize;      // 按钮尺寸
  disabled?: boolean;     // 是否禁用
  loading?: boolean;      // 是否显示加载状态
  onClick?: React.MouseEventHandler<HTMLElement>; // 点击事件
  children?: React.ReactNode; // 子元素
  className?: string;     // 自定义类名
  style?: React.CSSProperties; // 自定义样式
}

// 使用React.memo优化性能
const Button: React.FC<ButtonProps> = React.memo(({
  type = 'default',
  size = 'middle',
  disabled = false,
  loading = false,
  onClick,
  children,
  className = '',
  style,
}) => {
  // 合并类名
  const classes = [
    'btn',
    `btn-${type}`,
    `btn-${size}`,
    disabled ? 'btn-disabled' : '',
    loading ? 'btn-loading' : '',
    className,
  ].filter(Boolean).join(' ');

  return (
    <button
      className={classes}
      style={style}
      disabled={disabled || loading}
      onClick={onClick}
    >
      {loading && <span className="btn-loading-icon" />}
      {children}
    </button>
  );
});

export default Button;

配套的CSS样式也很重要,我们需要使用CSS Modules或者CSS-in-JS来避免样式冲突:

/* src/components/Button/Button.css */
.btn {
  position: relative;
  display: inline-block;
  font-weight: 400;
  white-space: nowrap;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s;
  user-select: none;
  outline: none;
  border: 1px solid transparent;
  border-radius: 4px;
}

/* 按钮大小 */
.btn-large {
  padding: 10px 20px;
  font-size: 16px;
}

.btn-middle {
  padding: 8px 16px;
  font-size: 14px;
}

.btn-small {
  padding: 6px 12px;
  font-size: 12px;
}

/* 按钮类型 */
.btn-primary {
  color: #fff;
  background: #1890ff;
  border-color: #1890ff;
}

.btn-primary:hover {
  background: #40a9ff;
  border-color: #40a9ff;
}

/* 其他按钮类型的样式... */

/* 加载状态 */
.btn-loading {
  opacity: 0.65;
  pointer-events: none;
}

.btn-loading-icon {
  display: inline-block;
  margin-right: 8px;
  /* 加载动画的实现... */
}

四、组件文档与展示

有了组件之后,我们需要一个好的方式来展示和测试它。这就是Storybook发挥作用的地方了。Storybook是一个独立的UI组件开发环境,可以让我们单独开发和测试组件。

让我们为Button组件创建一个Story:

// src/stories/Button.stories.tsx
import React from 'react';
import { Story, Meta } from '@storybook/react';
import Button, { ButtonProps } from '../components/Button/Button';

export default {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    type: {
      control: {
        type: 'select',
        options: ['primary', 'default', 'dashed', 'text', 'link'],
      },
    },
    size: {
      control: {
        type: 'select',
        options: ['large', 'middle', 'small'],
      },
    },
    onClick: { action: 'clicked' },
  },
} as Meta;

// 基础模板
const Template: Story<ButtonProps> = (args) => <Button {...args} />;

// 默认按钮
export const Default = Template.bind({});
Default.args = {
  children: 'Default Button',
};

// 主要按钮
export const Primary = Template.bind({});
Primary.args = {
  type: 'primary',
  children: 'Primary Button',
};

// 加载状态
export const Loading = Template.bind({});
Loading.args = {
  loading: true,
  children: 'Loading Button',
};

// 禁用状态
export const Disabled = Template.bind({});
Disabled.args = {
  disabled: true,
  children: 'Disabled Button',
};

运行npm run storybook,你就能看到一个漂亮的组件文档页面,可以交互式地测试各种props组合。

五、组件库的高级功能

基础组件完成后,我们可以考虑一些高级功能来提升组件库的实用性:

  1. 主题系统:允许用户自定义颜色、间距等设计变量
  2. 国际化:支持多语言
  3. 无障碍访问:确保残障人士也能使用
  4. 性能优化:减少不必要的渲染

让我们实现一个简单的主题系统:

// src/styles/theme.ts
export interface Theme {
  primaryColor: string;
  secondaryColor: string;
  fontSize: {
    small: string;
    medium: string;
    large: string;
  };
  spacing: {
    small: string;
    medium: string;
    large: string;
  };
}

// 默认主题
export const defaultTheme: Theme = {
  primaryColor: '#1890ff',
  secondaryColor: '#f5222d',
  fontSize: {
    small: '12px',
    medium: '14px',
    large: '16px',
  },
  spacing: {
    small: '8px',
    medium: '16px',
    large: '24px',
  },
};

// 暗色主题
export const darkTheme: Theme = {
  ...defaultTheme,
  primaryColor: '#177ddc',
  secondaryColor: '#d32029',
};

然后修改我们的Button组件来支持主题:

// src/components/Button/Button.tsx
import React from 'react';
import { defaultTheme } from '../../styles/theme';
import './Button.css';

// 创建主题上下文
const ThemeContext = React.createContext(defaultTheme);

// 修改ButtonProps接口
interface ButtonProps {
  // ...之前的props
  theme?: Partial<Theme>; // 可选的主题覆盖
}

const Button: React.FC<ButtonProps> = React.memo(({
  // ...之前的props
  theme,
}) => {
  const contextTheme = React.useContext(ThemeContext);
  const mergedTheme = { ...contextTheme, ...theme };
  
  // 使用主题中的变量
  const style = {
    ...props.style,
    fontSize: mergedTheme.fontSize.medium,
    padding: mergedTheme.spacing.medium,
  };

  // ...其余实现
});

// 导出ThemeProvider
export const ThemeProvider = ThemeContext.Provider;

六、组件测试与质量保证

一个企业级组件库必须有完善的测试。我们可以使用Jest和React Testing Library来编写测试用例。

// src/components/Button/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button组件', () => {
  test('渲染默认按钮', () => {
    render(<Button>测试按钮</Button>);
    const button = screen.getByText(/测试按钮/i);
    expect(button).toBeInTheDocument();
    expect(button).toHaveClass('btn', 'btn-default');
  });

  test('点击事件触发', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>点击我</Button>);
    fireEvent.click(screen.getByText(/点击我/i));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('禁用状态下不触发点击', () => {
    const handleClick = jest.fn();
    render(
      <Button disabled onClick={handleClick}>
        禁用按钮
      </Button>
    );
    fireEvent.click(screen.getByText(/禁用按钮/i));
    expect(handleClick).not.toHaveBeenCalled();
  });
});

七、打包与发布

最后,我们需要将组件库打包发布,让其他项目能够使用。我们可以使用rollup或webpack来打包。

首先安装必要的依赖:

npm install --save-dev @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-peer-deps-external rollup-plugin-postcss

然后创建rollup.config.js:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm',
      sourcemap: true,
    },
  ],
  plugins: [
    peerDepsExternal(),
    resolve(),
    commonjs(),
    typescript({ tsconfig: './tsconfig.json' }),
    postcss({
      modules: true,
      extract: true,
    }),
  ],
};

在package.json中添加脚本:

{
  "scripts": {
    "build": "rollup -c",
    "prepare": "npm run build"
  }
}

最后,创建一个入口文件src/index.ts来导出所有组件:

// src/index.ts
export { default as Button } from './components/Button/Button';
export { ThemeProvider } from './components/Button/Button';
// 导出其他组件...

八、实际应用与持续维护

组件库发布后,我们需要考虑如何在项目中实际使用它。首先将组件库发布到npm:

npm publish

然后在其他项目中安装:

npm install my-ui-library

使用示例:

import React from 'react';
import { Button, ThemeProvider } from 'my-ui-library';

function App() {
  return (
    <ThemeProvider>
      <Button type="primary" onClick={() => console.log('Clicked!')}>
        点击我
      </Button>
    </ThemeProvider>
  );
}

export default App;

持续维护方面,建议:

  1. 建立完善的变更日志
  2. 使用语义化版本控制
  3. 收集用户反馈
  4. 定期更新依赖
  5. 保持文档最新

九、总结与最佳实践

构建一个企业级React组件库是一个系统工程,需要考虑很多方面。以下是一些最佳实践:

  1. 设计先行:与设计师紧密合作,确保组件符合设计规范
  2. 渐进式开发:从简单组件开始,逐步添加复杂功能
  3. 文档为王:没有文档的组件等于不存在
  4. 测试覆盖:确保每个组件都有充分的测试
  5. 性能考量:注意组件渲染性能,避免不必要的重渲染
  6. 可访问性:确保所有用户都能使用你的组件
  7. 版本控制:遵循语义化版本规范

记住,一个好的组件库不是一蹴而就的,而是需要不断迭代和完善。随着业务需求的变化和技术的发展,你的组件库也需要不断进化。