一、为什么要造轮子:组件库开发的意义
在电商后台管理系统的开发中,我们团队曾经为了统一不同项目的UI风格,尝试过使用Element UI和Ant Design Vue。但随着业务场景的复杂化,现有组件库的扩展成本越来越高。比如需要给所有按钮加上动态波纹效果时,需要逐个修改node_modules里的源码,这促使我们决定自研组件库。
自研组件库的主要优势:
- 品牌风格定制:完全掌控视觉规范
- 功能深度定制:根据业务需求添加特定逻辑
- 性能优化空间:按需打包、精简依赖
- 统一开发规范:强制约束代码风格和API设计
二、技术选型与项目搭建
我们选择的技术栈组合:
- Vue 3.2+(组合式API)
- TypeScript 4.7+
- Vite 4.0+(构建工具)
- Sass(样式预处理)
- Vitest(单元测试)
初始化项目结构:
# 示例目录结构说明
├── packages # 组件源码
│ └── button
│ ├── src # 组件源代码
│ ├── __tests__ # 单元测试
│ └── index.ts # 组件入口
├── docs # 文档站点
├── play # 开发调试环境
└── package.json # 项目配置
配置基础构建文件(vite.config.ts):
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue({
reactivityTransform: true // 启用响应式语法糖
})],
build: {
lib: {
entry: 'packages/index.ts',
name: 'MyComponentLibrary',
fileName: 'my-lib'
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
})
三、开发第一个原子组件(Button)
<!-- packages/button/src/Button.vue -->
<template>
<button
:class="[
'my-button',
`my-button--${type}`,
{ 'is-disabled': disabled }
]"
:disabled="disabled"
@click="handleClick"
>
<!-- 插槽内容 -->
<slot />
</button>
</template>
<script lang="ts" setup>
import { withDefaults } from 'vue'
interface Props {
type?: 'primary' | 'success' | 'warning' | 'danger'
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
type: 'primary',
disabled: false
})
const emit = defineEmits(['click'])
const handleClick = (event: MouseEvent) => {
if (!props.disabled) {
emit('click', event)
}
}
</script>
<style lang="scss" scoped>
.my-button {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&--primary {
background: #409eff;
color: white;
}
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>
组件测试用例(Vitest示例):
// packages/button/__tests__/Button.test.ts
import { mount } from '@vue/test-utils'
import Button from '../src/Button.vue'
describe('Button Component', () => {
it('renders default type correctly', () => {
const wrapper = mount(Button)
expect(wrapper.classes()).toContain('my-button--primary')
})
it('emits click event when not disabled', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
})
it('prevents click when disabled', async () => {
const wrapper = mount(Button, {
props: { disabled: true }
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeUndefined()
})
})
四、复杂组件开发实战(图标选择器)
<!-- packages/icon-picker/src/IconPicker.vue -->
<template>
<div class="icon-picker">
<div
v-for="icon in filteredIcons"
:key="icon.name"
:class="['icon-item', { 'is-selected': selected === icon.name }]"
@click="selectIcon(icon.name)"
>
<component :is="icon.component" />
<span class="icon-name">{{ icon.name }}</span>
</div>
<input
v-model="searchText"
placeholder="搜索图标..."
class="search-input"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import * as icons from '@vicons/ionicons5' // 第三方图标库
interface IconItem {
name: string
component: any
}
const props = defineProps<{
selected?: string
}>()
const emit = defineEmits(['update:selected'])
const searchText = ref('')
const allIcons = Object.entries(icons).map(([name, component]) => ({
name: name.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`),
component
})) as IconItem[]
const filteredIcons = computed(() =>
allIcons.filter(icon =>
icon.name.toLowerCase().includes(searchText.value.toLowerCase())
)
)
const selectIcon = (name: string) => {
emit('update:selected', name)
}
</script>
五、文档系统建设(VitePress集成)
创建文档配置文件(.vitepress/config.js):
export default {
title: 'My Component Library',
themeConfig: {
sidebar: [
{
text: '指南',
items: [
{ text: '快速开始', link: '/guide/' },
{ text: '主题定制', link: '/guide/theming' }
]
},
{
text: '组件',
items: [
{ text: 'Button 按钮', link: '/components/button' },
{ text: 'IconPicker 图标选择器', link: '/components/icon-picker' }
]
}
]
}
}
六、构建优化与npm发布
package.json关键配置:
{
"name": "@yourname/my-component-library",
"version": "1.0.0",
"files": ["dist"],
"main": "dist/my-lib.umd.cjs",
"module": "dist/my-lib.es.js",
"exports": {
".": {
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.umd.cjs"
}
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
}
}
发布流程:
npm login
npm publish --access public
七、应用场景与技术选型
典型应用场景:
- 中后台管理系统(需要统一的交互规范)
- 多团队协作开发(强制统一代码风格)
- 跨平台应用(封装通用业务逻辑)
技术对比:
方案 | 维护成本 | 定制能力 | 学习成本 |
---|---|---|---|
自研组件库 | 高 | 100% | 中等 |
二次开发现有库 | 中 | 70% | 低 |
纯手工开发 | 低 | 0% | 高 |
八、开发陷阱与避坑指南
- 全局样式污染:使用scoped样式或CSS Module
- 版本锁定问题:通过peerDependencies声明Vue版本
- 类型声明丢失:导出类型定义文件(.d.ts)
- 浏览器兼容性:配置合适的polyfill
- 文档同步延迟:自动化文档生成(通过TS类型提取)
九、演进路线与未来展望
当组件库发展到50+组件时,建议采用这些优化策略:
- 引入按需加载(unplugin-vue-components)
- 开发可视化配置平台
- 集成自动化截图测试
- 构建主题配置系统
- 支持Web Components导出
十、文章总结
开发企业级Vue组件库需要完整的工程化思维,从原型设计到持续交付的每个环节都充满细节挑战。通过本文的实战演示,我们完成了从零到发布的全流程实践,重点攻克了类型安全、样式隔离、文档同步等常见痛点。在具体实践中,建议采用渐进式迭代策略,初期聚焦核心组件,逐步构建完整生态。