前端组件库是团队提效的核心基建之一。很多团队发展到一定规模,都会考虑从「复制粘贴组件」过渡到「统一维护的组件库」。但真正动手搭的时候,坑不少——技术选型、构建打包、按需加载、文档站点、发布流程,每一步都有选择。
这篇文章分享一下我从零搭建组件库的完整思路和实战经验。
目前主流方案就三个:
方案 | 优点 | 缺点 |
|---|---|---|
React + TypeScript | 生态最成熟,社区资源丰富 | 运行时体积较大 |
Vue 3 + TypeScript | Composition API 灵活,Tree-shaking 好 | 社区组件库竞争激烈 |
Web Components | 框架无关,原生支持 | 生态工具链不够完善 |
如果你的团队主要用 React,那 React + TypeScript + TSX 是标准答案。Vue 团队则选 Vue 3 + TypeScript + JSX/TSX。
我的推荐:CSS Modules + Less 作为基础方案,搭配 CSS Variables 做主题定制。
大型组件库推荐 Rollup + @rollup/plugin-typescript + esbuild(压缩)。
csharp 体验AI代码助手 代码解读复制代码my-ui/
├── packages/
│ ├── components/ # 组件源码
│ │ ├── Button/
│ │ │ ├── index.tsx
│ │ │ ├── style.ts
│ │ │ └── __tests__/
│ │ └── ...
│ ├── theme/ # 主题变量
│ │ ├── default.css
│ │ └── dark.css
│ └── utils/ # 工具函数
├── docs/ # 文档站点
├── scripts/ # 构建脚本
├── dist/ # 构建产物
└── package.jsonjs 体验AI代码助手 代码解读复制代码// 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 用户需要组件库应该支持按需加载,配合打包工具的 Tree-shaking:
json 体验AI代码助手 代码解读复制代码{
"sideEffects": ["**/*.css"],
"exports": {
".": "./dist/es/index.js",
"./Button": "./dist/es/Button/index.js",
"./Button/style": "./dist/es/Button/style.css"
}
}这样用户在引入时:
tsx 体验AI代码助手 代码解读复制代码// 全量引入
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 7 + MDX,一个命令启动开发环境,边开发边写文档,效率很高。
bash 体验AI代码助手 代码解读复制代码npx storybook@latest init --type react在 .storybook/main.ts 中配置:
ts 体验AI代码助手 代码解读复制代码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 体验AI代码助手 代码解读复制代码pnpm add -w @changesets/cli
pnpm changeset init工作流:
bash 体验AI代码助手 代码解读复制代码# 开发完成,记录变更
pnpm changeset
# 更新版本号 + 生成 changelog
pnpm changeset version
# 构建并发布
pnpm build
pnpm publishjson 体验AI代码助手 代码解读复制代码{
"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/"
}
}GitHub Actions 示例:
yaml 体验AI代码助手 代码解读复制代码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 体验AI代码助手 代码解读复制代码Button/
├── index.tsx # 组件实现
├── style.ts # 样式(或用独立的 CSS 文件)
├── interface.ts # 类型定义
├── __tests__/ # 单元测试
│ └── index.test.tsx
└── index.ts # 导出入口使用 React Testing Library + Vitest:
tsx 体验AI代码助手 代码解读复制代码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)
})
})forwardRef 让父组件能访问 DOMtsx 体验AI代码助手 代码解读复制代码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 体验AI代码助手 代码解读复制代码: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 体验AI代码助手 代码解读复制代码.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 变量。
对于多包场景(组件库 + 主题 + 工具函数),推荐 pnpm workspace:
yaml 体验AI代码助手 代码解读复制代码# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'docs'
- 'scripts'优势:
搭建组件库不是一个「写完代码就完事」的活。技术选型要匹配团队现状,构建配置要考虑按需加载和 Tree-shaking,文档和测试要跟上,发布流程要自动化。
如果你们团队还在「每个项目写一遍 Button」的阶段,不妨花一周时间搭个基础组件库。长期来看,这一周的投入回报率极高。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。