找回密码
 立即注册
首页 业界区 业界 组件库开发实战:从 0 到 1 搭建企业级组件库 ...

组件库开发实战:从 0 到 1 搭建企业级组件库

莠畅缕 前天 18:42
深夜,我盯着屏幕上的代码发呆。作为一个中型创业公司的前端负责人,我正在思考一个问题:我们是否需要开发自己的组件库?
过去一年,随着业务的快速发展,前端团队从 3 人扩展到了 12 人,项目也从 1 个变成了 6 个。每个项目都在用着不同版本的 UI 组件,设计规范也不统一,这让产品同学苦不堆言。经过一周的调研和团队讨论,我们决定开发一套自己的组件库。
项目规划

首先从业务目标出发,我们希望通过组件库实现三个核心目标:统一各个项目的设计规范、提高团队的开发效率、保证代码的质量标准。这些都是困扰我们团队已久的问题。
在技术层面,我们对组件库提出了严格的要求:必须有完整的 TypeScript 类型支持,确保类型安全;测试覆盖率要达到 100%,保证组件的可靠性;文档和示例要详细完整,降低使用门槛;同时要注重性能优化,保证组件的运行效率。
关于组件的范围,我们计划分四个层次逐步实现:

  • 基础组件:包括 Button、Input、Select、Table 等最常用的基础组件
  • 表单组件:包括 Form、DatePicker、Upload 等数据录入组件
  • 反馈组件:包括 Modal、Message、Notification 等交互反馈组件
  • 业务组件:包括 SearchForm、DetailCard、StatusFlow 等业务相关组件
这样的规划既照顾到了基础需求,又为未来的业务扩展预留了空间。我们决定采用渐进式开发策略,先完成基础组件和表单组件,然后再逐步扩展到反馈组件和业务组件。
技术选型


  • 在技术选型上,我们经过反复讨论和评估,最终确定了一套完整的技术栈。

  • 首先是基础框架的选择。考虑到团队的技术背景和项目需求,我们选择了 React 18 作为核心框架。样式解决方案上,我们采用了 Tailwind CSS 配合 CSS Modules 的组合,这样既能保证样式的可维护性,又能避免样式冲突。构建工具选择了 Vite,它的快速启动和热更新特性能大大提升开发效率。测试框架则采用了 Vitest 配合 Testing Library,这个组合既保证了测试的可靠性,又与 Vite 完美集成。

  • 在开发工具链方面,我们也做了精心的选择。文档系统采用了 Storybook,它不仅能够独立开发和测试组件,还能自动生成交互式文档。代码规范通过 ESLint 和 Prettier 的组合来保证,这样能确保团队代码风格的一致性。版本管理选择了 Changesets,它能够帮助我们更好地管理包的版本和变更日志。最后,我们使用 GitHub Actions 搭建了完整的 CI 流程,确保代码质量和发布流程的规范性。
组件开发

让我们从一个基础的 Button 组件开始:
  1. // components/Button/Button.tsx
  2. import { forwardRef } from 'react'
  3. import { cva, type VariantProps } from 'class-variance-authority'
  4. import { cn } from '@/utils'
  5. const buttonVariants = cva('inline-flex items-center justify-center rounded-md font-medium transition-colors', {
  6.   variants: {
  7.     variant: {
  8.       primary: 'bg-primary text-white hover:bg-primary/90',
  9.       secondary: 'bg-secondary text-white hover:bg-secondary/90',
  10.       outline: 'border-2 border-primary text-primary hover:bg-primary/10'
  11.     },
  12.     size: {
  13.       sm: 'h-8 px-3 text-sm',
  14.       md: 'h-10 px-4 text-base',
  15.       lg: 'h-12 px-6 text-lg'
  16.     }
  17.   },
  18.   defaultVariants: {
  19.     variant: 'primary',
  20.     size: 'md'
  21.   }
  22. })
  23. interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
  24.   loading?: boolean
  25.   icon?: React.ReactNode
  26. }
  27. const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, loading, icon, children, ...props }, ref) => {
  28.   return (
  29.     <button ref={ref} className={cn(buttonVariants({ variant, size }), className)} disabled={loading || props.disabled} {...props}>
  30.       {loading && <LoadingSpinner className='mr-2 h-4 w-4 animate-spin' />}
  31.       {icon && {icon}}
  32.       {children}
  33.     </button>
  34.   )
  35. })
  36. Button.displayName = 'Button'
  37. export { Button, buttonVariants }
复制代码
测试用例

每个组件都需要完整的测试用例:
  1. // components/Button/Button.test.tsx
  2. import { render, screen, fireEvent } from '@testing-library/react'
  3. import { Button } from './Button'
  4. describe('Button', () => {
  5.   it('renders children correctly', () => {
  6.     render(<Button>Click me</Button>)
  7.     expect(screen.getByText('Click me')).toBeInTheDocument()
  8.   })
  9.   it('handles different variants', () => {
  10.     const { rerender } = render(<Button variant='primary'>Primary</Button>)
  11.     expect(screen.getByText('Primary')).toHaveClass('bg-primary')
  12.     rerender(<Button variant='secondary'>Secondary</Button>)
  13.     expect(screen.getByText('Secondary')).toHaveClass('bg-secondary')
  14.   })
  15.   it('shows loading state', () => {
  16.     render(<Button loading>Loading</Button>)
  17.     expect(screen.getByRole('button')).toBeDisabled()
  18.     expect(screen.getByText('Loading')).toBeInTheDocument()
  19.     expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
  20.   })
  21.   it('forwards ref correctly', () => {
  22.     const ref = jest.fn()
  23.     render(<Button ref={ref}>Click me</Button>)
  24.     expect(ref).toHaveBeenCalledWith(expect.any(HTMLButtonElement))
  25.   })
  26. })
复制代码
文档系统

好的文档对组件库至关重要:
  1. // stories/Button.stories.tsx
  2. import type { Meta, StoryObj } from '@storybook/react'
  3. import { Button } from '@/components/Button'
  4. const meta: Meta<typeof Button> = {
  5.   title: 'Components/Button',
  6.   component: Button,
  7.   tags: ['autodocs'],
  8.   argTypes: {
  9.     variant: {
  10.       control: 'select',
  11.       options: ['primary', 'secondary', 'outline']
  12.     },
  13.     size: {
  14.       control: 'select',
  15.       options: ['sm', 'md', 'lg']
  16.     }
  17.   }
  18. }
  19. export default meta
  20. type Story = StoryObj<typeof Button>
  21. export const Primary: Story = {
  22.   args: {
  23.     children: 'Primary Button',
  24.     variant: 'primary'
  25.   }
  26. }
  27. export const WithIcon: Story = {
  28.   args: {
  29.     children: 'With Icon',
  30.     icon: <Icon name='plus' />,
  31.     variant: 'primary'
  32.   }
  33. }
  34. export const Loading: Story = {
  35.   args: {
  36.     children: 'Loading',
  37.     loading: true
  38.   }
  39. }
复制代码
发布流程

建立规范的发布流程很重要:
  1. // scripts/release.ts
  2. import { getChangedPackages, createChangeset, publishPackages } from '@changesets/cli'
  3. async function release() {
  4.   // 获取变更的包
  5.   const changedPackages = await getChangedPackages()
  6.   // 创建变更集
  7.   await createChangeset({
  8.     summary: 'Update components and fix bugs',
  9.     packages: changedPackages.map(pkg => ({
  10.       name: pkg.name,
  11.       type: 'patch' // major | minor | patch
  12.     }))
  13.   })
  14.   // 发布包
  15.   await publishPackages({
  16.     tag: 'latest',
  17.     access: 'public',
  18.     registry: 'https://registry.npmjs.org'
  19.   })
  20. }
  21. release().catch(console.error)
复制代码
实践心得

开发组件库的过程中,我们总结出几点重要经验:

  • 组件设计要兼顾灵活性和易用性
  • TypeScript 类型定义要完整准确
  • 文档和示例要详细易懂
  • 测试用例要覆盖各种场景
  • 版本管理要规范严谨
就像搭建一座大楼,组件库需要有坚实的基础(架构设计),规范的施工流程(开发规范),以及完善的配套设施(文档测试)。只有这样,才能建造出一个稳固耐用的组件库。
写在最后

开发组件库是一个持续演进的过程,需要在实践中不断完善。正如建筑需要经常维护一样,组件库也需要持续的更新和优化。
有什么问题欢迎在评论区讨论,让我们一起探讨组件库开发的最佳实践!
如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册