前言
很多前端工程师用过 Element Plus、Ant Design,但自己动手搭建组件库时却无从下手。本文从零开始,带你搭建一个生产级组件库,包含设计系统、工程化、文档、发布全流程。
正文
一、组件库架构设计
1.1 整体架构
my-ui/
├── packages/
│ ├── components/ # 组件源码
│ ├── theme/ # 样式主题
│ └── utils/ # 工具函数
├── docs/ # 文档站点
├── play/ # 调试 playground
├── scripts/ # 构建脚本
└── package.json
1.2 技术选型(2026年标准)
| 环节 | 工具 | 说明 |
|---|---|---|
| 构建 | Rollup + Vite | 支持 ESM/CJS/UMD |
| 测试 | Vitest | 单元测试 |
| 文档 | Storybook | 组件展示 |
| 类型 | TypeScript | 类型定义 |
| 样式 | SCSS + CSS Vars | 主题定制 |
| 发布 | Changeset | 版本管理 |
二、组件设计与实现
2.1 Button 组件示例
typescript
// packages/components/button/src/button.tsx
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'MyButton',
props: {
type: {
type: String as PropType<'primary' | 'success' | 'warning' | 'danger'>,
default: 'primary'
},
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium'
},
disabled: Boolean,
loading: Boolean
},
setup(props, { slots, emit }) {
return () => (
<button
class={[
'my-button',
`my-button--${props.type}`,
`my-button--${props.size}`,
{
'is-disabled': props.disabled,
'is-loading': props.loading
}
]}
disabled={props.disabled || props.loading}
onClick={() => emit('click')}
>
{props.loading && <span class="my-button__loading" />}
{slots.default?.()}
</button>
)
}
})
2.2 样式设计(CSS 变量方案)
scss
// packages/theme/src/button.scss
:root {
--my-button-primary-bg: #409eff;
--my-button-primary-text: #ffffff;
--my-button-success-bg: #67c23a;
--my-button-danger-bg: #f56c6c;
--my-button-small-padding: 8px 12px;
--my-button-medium-padding: 12px 20px;
--my-button-large-padding: 16px 24px;
}
.my-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&--primary {
background: var(--my-button-primary-bg);
color: var(--my-button-primary-text);
&:hover {
opacity: 0.8;
}
}
&--small {
padding: var(--my-button-small-padding);
font-size: 12px;
}
&--medium {
padding: var(--my-button-medium-padding);
font-size: 14px;
}
}
2.3 类型定义文件
typescript
// packages/components/button/src/button.d.ts
export type ButtonType = 'primary' | 'success' | 'warning' | 'danger'
export type ButtonSize = 'small' | 'medium' | 'large'
export interface ButtonProps {
type?: ButtonType
size?: ButtonSize
disabled?: boolean
loading?: boolean
}
export interface ButtonEmits {
(e: 'click'): void
}
三、工程化配置
3.1 Rollup 构建配置
javascript
// scripts/rollup.config.js
import { defineConfig } from 'rollup'
import vue from '@vitejs/plugin-vue'
import typescript from '@rollup/plugin-typescript'
import postcss from 'rollup-plugin-postcss'
import { terser } from 'rollup-plugin-terser'
export default defineConfig([
// ESM 构建
{
input: 'packages/components/index.ts',
output: {
file: 'dist/es/index.js',
format: 'es'
},
plugins: [
vue(),
typescript(),
postcss({
extract: true,
minimize: true
})
],
external: ['vue']
},
// CJS 构建
{
input: 'packages/components/index.ts',
output: {
file: 'dist/lib/index.js',
format: 'cjs'
},
plugins: [vue(), typescript()],
external: ['vue']
},
// UMD 构建(浏览器直接用)
{
input: 'packages/components/index.ts',
output: {
file: 'dist/dist/index.full.js',
format: 'umd',
name: 'MyUI',
globals: {
vue: 'Vue'
}
},
plugins: [vue(), typescript(), terser()],
external: ['vue']
}
])
3.2 package.json 配置
json
{
"name": "@my-org/ui",
"version": "0.1.0",
"type": "module",
"main": "dist/lib/index.js",
"module": "dist/es/index.js",
"types": "dist/types/index.d.ts",
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "rollup -c scripts/rollup.config.js",
"build:types": "tsc --declaration --emitDeclarationOnly --outDir dist/types",
"test": "vitest",
"docs:dev": "storybook dev -p 6006",
"docs:build": "storybook build -o docs-dist",
"release": "changeset publish"
},
"peerDependencies": {
"vue": "^3.3.0"
}
}
四、测试策略
4.1 单元测试(Vitest)
typescript
// packages/components/button/__tests__/button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../src/button'
describe('Button', () => {
it('renders correctly', () => {
const wrapper = mount(Button, {
slots: { default: 'Click me' }
})
expect(wrapper.text()).toBe('Click me')
expect(wrapper.classes()).toContain('my-button')
})
it('handles different types', () => {
const wrapper = mount(Button, {
props: { type: 'success' }
})
expect(wrapper.classes()).toContain('my-button--success')
})
it('emits click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
})
it('is disabled when loading', () => {
const wrapper = mount(Button, {
props: { loading: true }
})
expect(wrapper.attributes('disabled')).toBeDefined()
})
})
4.2 视觉回归测试(Storybook + Chromatic)
typescript
// packages/components/button/src/button.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import Button from './button'
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
argTypes: {
type: { control: 'select', options: ['primary', 'success', 'warning', 'danger'] },
size: { control: 'select', options: ['small', 'medium', 'large'] }
}
}
export default meta
type Story = StoryObj<typeof Button>
export const Primary: Story = {
args: {
type: 'primary',
default: 'Primary Button'
}
}
export const Loading: Story = {
args: {
loading: true,
default: 'Loading...'
}
}
五、文档站点搭建
5.1 VitePress 配置
javascript
// docs/.vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'My UI',
description: 'A Vue 3 Component Library',
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide/' },
{ text: 'Components', link: '/components/' }
],
sidebar: {
'/components/': [
{
text: 'Basic',
items: [
{ text: 'Button', link: '/components/button' },
{ text: 'Icon', link: '/components/icon' }
]
},
{
text: 'Form',
items: [
{ text: 'Input', link: '/components/input' },
{ text: 'Select', link: '/components/select' }
]
}
]
}
}
})
六、发布与版本管理
6.1 Changeset 配置
json
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
6.2 发布流程
bash
# 1. 添加 changeset
npx changeset
# 2. 提升版本
npx changeset version
# 3. 构建
npm run build
# 4. 发布
npx changeset publish
七、设计系统对接
7.1 Figma Tokens 同步
javascript
// scripts/sync-figma-tokens.js
import { getFigmaFile } from 'figma-api'
async function syncTokens() {
const file = await getFigmaFile('FILE_KEY')
const tokens = extractTokens(file)
// 生成 CSS 变量
generateCSSVariables(tokens)
// 生成 TypeScript 类型
generateTSTypes(tokens)
}
八、总结
搭建组件库不是简单的代码堆砌,而是系统工程:
- 设计先行 - 有完整的设计系统和规范
- 质量保障 - 测试覆盖、类型完整
- 开发体验 - 文档完善、调试方便
- 发布流程 - 自动化、版本管理
2026年组件库新趋势:
- AI 辅助设计(Figma AI 生成组件)
- Headless UI 模式(逻辑与样式分离)
- 微前端适配(Module Federation 支持)