从零搭建前端组件库:一套完整的技术选型与工程化实践

从零搭建前端组件库:一套完整的技术选型与工程化实践

前言

前端组件库是团队提效的核心基建之一。很多团队发展到一定规模,都会考虑从「复制粘贴组件」过渡到「统一维护的组件库」。但真正动手搭的时候,坑不少------技术选型、构建打包、按需加载、文档站点、发布流程,每一步都有选择。

这篇文章分享一下我从零搭建组件库的完整思路和实战经验。

一、技术选型

框架选择

目前主流方案就三个:

方案 优点 缺点
React + TypeScript 生态最成熟,社区资源丰富 运行时体积较大
Vue 3 + TypeScript Composition API 灵活,Tree-shaking 好 社区组件库竞争激烈
Web Components 框架无关,原生支持 生态工具链不够完善

如果你的团队主要用 React,那 React + TypeScript + TSX 是标准答案。Vue 团队则选 Vue 3 + TypeScript + JSX/TSX

样式方案

  • CSS Modules:零运行时,原生支持,推荐
  • styled-components / emotion:运行时 CSS-in-JS,灵活但引入额外体积
  • UnoCSS / Tailwind:原子化 CSS,适合工具类组件,但在复杂组件中有局限性
  • Less / Sass:预处理器,成熟稳定

我的推荐:CSS Modules + Less 作为基础方案,搭配 CSS Variables 做主题定制。

构建工具

  • Rollup:组件库打包首选,Tree-shaking 最佳,输出格式灵活(ESM / CJS / UMD)
  • esbuild:极快的构建速度,适合作为 Rollup 插件使用
  • tsup:基于 esbuild,零配置发布 TS 库,适合中小型组件库

大型组件库推荐 Rollup + @rollup/plugin-typescript + esbuild(压缩)。

二、目录结构设计

csharp 复制代码
my-ui/
├── packages/
│   ├── components/         # 组件源码
│   │   ├── Button/
│   │   │   ├── index.tsx
│   │   │   ├── style.ts
│   │   │   └── __tests__/
│   │   └── ...
│   ├── theme/              # 主题变量
│   │   ├── default.css
│   │   └── dark.css
│   └── utils/              # 工具函数
├── docs/                   # 文档站点
├── scripts/                # 构建脚本
├── dist/                   # 构建产物
└── package.json

三、构建打包配置

Rollup 配置核心要点

js 复制代码
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import postcss from 'rollup-plugin-postcss'
import { terser } from 'rollup-plugin-terser'

export default [
  // ESM 输出
  {
    input: 'packages/index.ts',
    output: { dir: 'dist/es', format: 'esm' },
    plugins: [
      resolve(),
      commonjs(),
      typescript({ declaration: true, declarationDir: 'dist/es' }),
      postcss({ modules: true, extract: true }),
    ],
    external: ['react', 'react-dom'],
  },
  // UMD 输出
  {
    input: 'packages/index.ts',
    output: { dir: 'dist/umd', format: 'umd', name: 'MyUI' },
    plugins: [
      resolve(),
      commonjs(),
      typescript(),
      postcss({ modules: true }),
      terser(),
    ],
    external: ['react', 'react-dom'],
  },
]

关键点:

  1. react / vue 设为 external,避免打包进库
  2. 生成 .d.ts 类型声明文件,TypeScript 用户需要
  3. CSS 文件单独输出,不要内联到 JS 里

按需加载

组件库应该支持按需加载,配合打包工具的 Tree-shaking:

json 复制代码
{
  "sideEffects": ["**/*.css"],
  "exports": {
    ".": "./dist/es/index.js",
    "./Button": "./dist/es/Button/index.js",
    "./Button/style": "./dist/es/Button/style.css"
  }
}

这样用户在引入时:

tsx 复制代码
// 全量引入
import { Button } from 'my-ui'
import 'my-ui/dist/es/index.css'

// 按需引入(需要 babel-plugin-import 或手动)
import Button from 'my-ui/es/Button'
import 'my-ui/es/Button/style.css'

四、文档站点

推荐方案:

  • Storybook:组件开发环境 + 文档一体,交互式文档,自动生成Props表格
  • Docusaurus / VitePress:更适合纯文档站点,结合 MDX 写组件示例

我选择 Storybook 7 + MDX,一个命令启动开发环境,边开发边写文档,效率很高。

bash 复制代码
npx storybook@latest init --type react

.storybook/main.ts 中配置:

ts 复制代码
import { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
  stories: ['../packages/**/*.stories.@(ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: '@storybook/react-vite',
}

每个组件写一个 .stories.mdx,既作为开发时的 playground,也作为发布后的文档。

五、发布流程

版本管理

推荐 Changeset:

bash 复制代码
pnpm add -w @changesets/cli
pnpm changeset init

工作流:

bash 复制代码
# 开发完成,记录变更
pnpm changeset
# 更新版本号 + 生成 changelog
pnpm changeset version
# 构建并发布
pnpm build
pnpm publish

NPM 发布配置

json 复制代码
{
  "name": "@your-team/my-ui",
  "version": "0.1.0",
  "main": "dist/cjs/index.js",
  "module": "dist/es/index.js",
  "types": "dist/es/index.d.ts",
  "files": ["dist"],
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  }
}

CI/CD 集成

GitHub Actions 示例:

yaml 复制代码
name: Release
on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://registry.npmjs.org/
      - run: pnpm install
      - run: pnpm build
      - run: pnpm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

六、组件开发规范

组件结构

每个组件遵循统一结构:

csharp 复制代码
Button/
├── index.tsx          # 组件实现
├── style.ts           # 样式(或用独立的 CSS 文件)
├── interface.ts       # 类型定义
├── __tests__/         # 单元测试
│   └── index.test.tsx
└── index.ts           # 导出入口

测试

使用 React Testing Library + Vitest:

tsx 复制代码
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './index'

describe('Button', () => {
  it('renders correctly', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('fires onClick when clicked', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

组件 API 设计原则

  1. 单一职责:一个组件只做一件事
  2. 受控 + 非受控:支持两种模式,让使用者灵活选择
  3. 合理的默认值:开箱即用,但不强加意志
  4. 类型安全:完整的 TypeScript 泛型和类型推导
  5. Ref 转发 :使用 forwardRef 让父组件能访问 DOM
tsx 复制代码
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  loading?: boolean
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', loading, children, ...rest }, ref) => {
    return (
      <button
        ref={ref}
        className={clsx('my-btn', `my-btn--${variant}`, `my-btn--${size}`)}
        disabled={loading || rest.disabled}
        {...rest}
      >
        {loading && <span className="my-btn__spinner" />}
        {children}
      </button>
    )
  }
)

七、主题与样式定制

CSS Variables 方案

css 复制代码
:root {
  --my-primary: #1677ff;
  --my-primary-hover: #4096ff;
  --my-border-radius: 6px;
  --my-font-size: 14px;
  --my-spacing-unit: 4px;
}

[data-theme='dark'] {
  --my-primary: #1668dc;
  --my-primary-hover: #3c89e8;
}

组件内统一使用 CSS Variables:

css 复制代码
.my-btn--primary {
  background: var(--my-primary);
  border-radius: var(--my-border-radius);
  font-size: var(--my-font-size);
}

.my-btn--primary:hover {
  background: var(--my-primary-hover);
}

用户可以通过覆盖变量轻松定制主题,无需使用 less/sass 变量。

八、Monorepo 管理

对于多包场景(组件库 + 主题 + 工具函数),推荐 pnpm workspace

yaml 复制代码
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'docs'
  - 'scripts'

优势:

  • 依赖共享,节省磁盘空间
  • 自动 link,开发体验好
  • 配合 Changeset 做版本管理

总结

搭建组件库不是一个「写完代码就完事」的活。技术选型要匹配团队现状,构建配置要考虑按需加载和 Tree-shaking,文档和测试要跟上,发布流程要自动化。

如果你们团队还在「每个项目写一遍 Button」的阶段,不妨花一周时间搭个基础组件库。长期来看,这一周的投入回报率极高。

最后列几个值得参考的开源组件库,可以从它们的源码里学到很多:

  • Ant Design --- 设计规范 + 组件库的标杆
  • Arco Design --- 字节出品,Monorepo 管理优秀
  • Radix UI --- 无样式组件,关注可访问性
  • shadcn/ui --- 不发布 npm 包,直接复制代码,很有启发的模式

希望这篇文章对你有帮助,欢迎在评论区交流!

相关推荐
小短腿的代码世界2 小时前
Qt OpenGL 架构与自定义着色器:源码级解析高性能图形渲染
qt·架构·着色器
一切皆是因缘际会4 小时前
2026实战:AI可解释性落地全指南
人工智能·深度学习·机器学习·架构
坐吃山猪4 小时前
【Hanako】README08_LEVEL4_插件系统架构
python·架构·agent·源码阅读
预知同行6 小时前
多模态模型架构三代演进:从双塔对齐到原生统一的设计哲学
架构
SamDeepThinking6 小时前
拼单模块设计实战
java·后端·架构
富士康质检员张全蛋6 小时前
Kafka架构 数据发送保障
分布式·架构·kafka
小短腿的代码世界7 小时前
Qt 3D 深度解析:QtQuick 与 Scene Graph 驱动的工业级 3D 渲染架构
qt·3d·架构
无尽冬.7 小时前
个人八股之三层架构
java·经验分享·后端·架构·异世界
花椒技术8 小时前
AI 协同开发落地复盘:1 小时生成首版后,为什么 Review 和修正又花了 2-3 天
前端·人工智能·架构