从零搭建前端组件库:一套完整的技术选型与工程化实践
前言
前端组件库是团队提效的核心基建之一。很多团队发展到一定规模,都会考虑从「复制粘贴组件」过渡到「统一维护的组件库」。但真正动手搭的时候,坑不少------技术选型、构建打包、按需加载、文档站点、发布流程,每一步都有选择。
这篇文章分享一下我从零搭建组件库的完整思路和实战经验。
一、技术选型
框架选择
目前主流方案就三个:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 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'],
},
]
关键点:
- 将
react/vue设为 external,避免打包进库 - 生成
.d.ts类型声明文件,TypeScript 用户需要 - 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 设计原则
- 单一职责:一个组件只做一件事
- 受控 + 非受控:支持两种模式,让使用者灵活选择
- 合理的默认值:开箱即用,但不强加意志
- 类型安全:完整的 TypeScript 泛型和类型推导
- 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 包,直接复制代码,很有启发的模式
希望这篇文章对你有帮助,欢迎在评论区交流!