组件库
使用 VitePress 构建现代化的 Vue 组件库,提供完整的组件开发、测试、文档和发布解决方案。
项目概述
组件库是前端开发中的重要基础设施,它提供了一套统一的 UI 组件和设计规范。本项目展示如何使用 VitePress 构建一个完整的 Vue 组件库,包含组件开发、文档编写、测试覆盖和包发布等全流程。
核心特性
- 🎨 现代设计 - 基于最新设计系统的美观组件
- 📦 开箱即用 - 完整的组件库开发工具链
- 📚 完善文档 - 详细的使用文档和示例代码
- 🧪 测试覆盖 - 完整的单元测试和 E2E 测试
- 🔧 TypeScript - 完整的类型定义和智能提示
- 🎯 按需加载 - 支持 Tree Shaking 和按需导入
- 🌍 国际化 - 内置多语言支持
- 📱 响应式 - 完美适配各种设备尺寸
- 🎭 主题定制 - 灵活的主题配置系统
- 🚀 性能优化 - 轻量级且高性能
技术架构
核心技术栈
json
{
"framework": "Vue 3",
"language": "TypeScript",
"bundler": "Vite",
"docs": "VitePress",
"testing": "Vitest + Vue Test Utils",
"styling": "CSS Variables + PostCSS",
"tools": [
"ESLint",
"Prettier",
"Husky",
"Commitizen",
"Semantic Release"
]
}
项目结构
vue-ui-library/
├── packages/
│ ├── components/ # 组件源码
│ │ ├── button/
│ │ ├── input/
│ │ ├── modal/
│ │ └── index.ts
│ ├── theme/ # 主题样式
│ │ ├── variables.css
│ │ ├── base.css
│ │ └── components.css
│ └── utils/ # 工具函数
├── docs/ # 文档站点
│ ├── .vitepress/
│ ├── components/
│ ├── guide/
│ └── examples/
├── playground/ # 开发调试
├── tests/ # 测试文件
├── scripts/ # 构建脚本
└── package.json
实现步骤
1. 项目初始化
bash
# 创建项目
mkdir vue-ui-library
cd vue-ui-library
# 初始化 monorepo
npm init -y
npm install -D @changesets/cli
# 安装核心依赖
npm install vue@^3.3.0
npm install -D @vitejs/plugin-vue typescript @vue/tsconfig vite
2. 配置开发环境
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: resolve(__dirname, 'packages/components/index.ts'),
name: 'VueUILibrary',
fileName: (format) => `vue-ui-library.${format}.js`
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
},
resolve: {
alias: {
'@': resolve(__dirname, 'packages')
}
}
})
3. 组件开发
vue
<!-- packages/components/button/Button.vue -->
<template>
<button
:class="buttonClass"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="ui-button__loading">
<LoadingIcon />
</span>
<span v-if="icon && !loading" class="ui-button__icon">
<component :is="icon" />
</span>
<span class="ui-button__content">
<slot />
</span>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { ButtonProps, ButtonEmits } from './types'
import LoadingIcon from '../icons/LoadingIcon.vue'
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'default',
size: 'medium',
disabled: false,
loading: false,
block: false,
round: false
})
const emit = defineEmits<ButtonEmits>()
const buttonClass = computed(() => [
'ui-button',
`ui-button--${props.type}`,
`ui-button--${props.size}`,
{
'ui-button--disabled': props.disabled,
'ui-button--loading': props.loading,
'ui-button--block': props.block,
'ui-button--round': props.round
}
])
function handleClick(event: MouseEvent) {
if (props.disabled || props.loading) return
emit('click', event)
}
</script>
<style scoped>
.ui-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: var(--ui-button-padding);
border: var(--ui-button-border);
border-radius: var(--ui-button-border-radius);
background: var(--ui-button-bg);
color: var(--ui-button-color);
font-size: var(--ui-button-font-size);
font-weight: var(--ui-button-font-weight);
line-height: 1.5;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
white-space: nowrap;
}
.ui-button:hover:not(.ui-button--disabled) {
background: var(--ui-button-hover-bg);
border-color: var(--ui-button-hover-border);
color: var(--ui-button-hover-color);
}
.ui-button:active:not(.ui-button--disabled) {
background: var(--ui-button-active-bg);
border-color: var(--ui-button-active-border);
color: var(--ui-button-active-color);
}
.ui-button--disabled {
opacity: 0.6;
cursor: not-allowed;
}
.ui-button--loading {
cursor: not-allowed;
}
.ui-button--block {
width: 100%;
}
.ui-button--round {
border-radius: 50px;
}
/* 尺寸变体 */
.ui-button--small {
--ui-button-padding: 4px 12px;
--ui-button-font-size: 12px;
--ui-button-border-radius: 4px;
}
.ui-button--medium {
--ui-button-padding: 8px 16px;
--ui-button-font-size: 14px;
--ui-button-border-radius: 6px;
}
.ui-button--large {
--ui-button-padding: 12px 24px;
--ui-button-font-size: 16px;
--ui-button-border-radius: 8px;
}
/* 类型变体 */
.ui-button--default {
--ui-button-bg: var(--ui-color-white);
--ui-button-color: var(--ui-color-text);
--ui-button-border: 1px solid var(--ui-color-border);
--ui-button-hover-bg: var(--ui-color-gray-50);
--ui-button-hover-border: var(--ui-color-border-hover);
--ui-button-hover-color: var(--ui-color-text);
--ui-button-active-bg: var(--ui-color-gray-100);
--ui-button-active-border: var(--ui-color-border-active);
--ui-button-active-color: var(--ui-color-text);
}
.ui-button--primary {
--ui-button-bg: var(--ui-color-primary);
--ui-button-color: var(--ui-color-white);
--ui-button-border: 1px solid var(--ui-color-primary);
--ui-button-hover-bg: var(--ui-color-primary-hover);
--ui-button-hover-border: var(--ui-color-primary-hover);
--ui-button-hover-color: var(--ui-color-white);
--ui-button-active-bg: var(--ui-color-primary-active);
--ui-button-active-border: var(--ui-color-primary-active);
--ui-button-active-color: var(--ui-color-white);
}
.ui-button--success {
--ui-button-bg: var(--ui-color-success);
--ui-button-color: var(--ui-color-white);
--ui-button-border: 1px solid var(--ui-color-success);
--ui-button-hover-bg: var(--ui-color-success-hover);
--ui-button-hover-border: var(--ui-color-success-hover);
--ui-button-hover-color: var(--ui-color-white);
--ui-button-active-bg: var(--ui-color-success-active);
--ui-button-active-border: var(--ui-color-success-active);
--ui-button-active-color: var(--ui-color-white);
}
.ui-button--warning {
--ui-button-bg: var(--ui-color-warning);
--ui-button-color: var(--ui-color-white);
--ui-button-border: 1px solid var(--ui-color-warning);
--ui-button-hover-bg: var(--ui-color-warning-hover);
--ui-button-hover-border: var(--ui-color-warning-hover);
--ui-button-hover-color: var(--ui-color-white);
--ui-button-active-bg: var(--ui-color-warning-active);
--ui-button-active-border: var(--ui-color-warning-active);
--ui-button-active-color: var(--ui-color-white);
}
.ui-button--danger {
--ui-button-bg: var(--ui-color-danger);
--ui-button-color: var(--ui-color-white);
--ui-button-border: 1px solid var(--ui-color-danger);
--ui-button-hover-bg: var(--ui-color-danger-hover);
--ui-button-hover-border: var(--ui-color-danger-hover);
--ui-button-hover-color: var(--ui-color-white);
--ui-button-active-bg: var(--ui-color-danger-active);
--ui-button-active-border: var(--ui-color-danger-active);
--ui-button-active-color: var(--ui-color-white);
}
.ui-button__loading {
animation: spin 1s linear infinite;
}
.ui-button__icon {
display: flex;
align-items: center;
}
.ui-button__content {
display: flex;
align-items: center;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
4. 类型定义
typescript
// packages/components/button/types.ts
import type { Component } from 'vue'
export type ButtonType = 'default' | 'primary' | 'success' | 'warning' | 'danger'
export type ButtonSize = 'small' | 'medium' | 'large'
export interface ButtonProps {
type?: ButtonType
size?: ButtonSize
disabled?: boolean
loading?: boolean
block?: boolean
round?: boolean
icon?: Component | string
}
export interface ButtonEmits {
click: [event: MouseEvent]
}
5. 组件导出
typescript
// packages/components/button/index.ts
import Button from './Button.vue'
import type { App } from 'vue'
Button.install = (app: App) => {
app.component('UiButton', Button)
}
export default Button
export * from './types'
typescript
// packages/components/index.ts
import type { App } from 'vue'
import Button from './button'
import Input from './input'
import Modal from './modal'
// ... 其他组件
const components = [
Button,
Input,
Modal
// ... 其他组件
]
const install = (app: App) => {
components.forEach(component => {
app.use(component)
})
}
export {
Button,
Input,
Modal,
install
}
export default {
install
}
6. 主题系统
css
/* packages/theme/variables.css */
:root {
/* 颜色系统 */
--ui-color-primary: #1890ff;
--ui-color-primary-hover: #40a9ff;
--ui-color-primary-active: #096dd9;
--ui-color-success: #52c41a;
--ui-color-success-hover: #73d13d;
--ui-color-success-active: #389e0d;
--ui-color-warning: #faad14;
--ui-color-warning-hover: #ffc53d;
--ui-color-warning-active: #d48806;
--ui-color-danger: #ff4d4f;
--ui-color-danger-hover: #ff7875;
--ui-color-danger-active: #d9363e;
--ui-color-white: #ffffff;
--ui-color-black: #000000;
--ui-color-gray-50: #fafafa;
--ui-color-gray-100: #f5f5f5;
--ui-color-gray-200: #f0f0f0;
--ui-color-gray-300: #d9d9d9;
--ui-color-gray-400: #bfbfbf;
--ui-color-gray-500: #8c8c8c;
--ui-color-gray-600: #595959;
--ui-color-gray-700: #434343;
--ui-color-gray-800: #262626;
--ui-color-gray-900: #1f1f1f;
/* 文本颜色 */
--ui-color-text: rgba(0, 0, 0, 0.85);
--ui-color-text-secondary: rgba(0, 0, 0, 0.65);
--ui-color-text-disabled: rgba(0, 0, 0, 0.25);
/* 边框颜色 */
--ui-color-border: #d9d9d9;
--ui-color-border-hover: #40a9ff;
--ui-color-border-active: #1890ff;
/* 背景颜色 */
--ui-color-bg: #ffffff;
--ui-color-bg-secondary: #fafafa;
--ui-color-bg-disabled: #f5f5f5;
/* 阴影 */
--ui-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--ui-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--ui-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--ui-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* 圆角 */
--ui-radius-sm: 2px;
--ui-radius: 4px;
--ui-radius-md: 6px;
--ui-radius-lg: 8px;
--ui-radius-xl: 12px;
/* 间距 */
--ui-space-xs: 4px;
--ui-space-sm: 8px;
--ui-space: 12px;
--ui-space-md: 16px;
--ui-space-lg: 20px;
--ui-space-xl: 24px;
--ui-space-2xl: 32px;
/* 字体 */
--ui-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--ui-font-family-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--ui-font-size-xs: 12px;
--ui-font-size-sm: 14px;
--ui-font-size: 16px;
--ui-font-size-lg: 18px;
--ui-font-size-xl: 20px;
--ui-font-size-2xl: 24px;
--ui-font-size-3xl: 30px;
--ui-font-weight-normal: 400;
--ui-font-weight-medium: 500;
--ui-font-weight-semibold: 600;
--ui-font-weight-bold: 700;
/* 行高 */
--ui-line-height-tight: 1.25;
--ui-line-height-normal: 1.5;
--ui-line-height-relaxed: 1.75;
/* 动画 */
--ui-transition-fast: 0.1s ease;
--ui-transition: 0.2s ease;
--ui-transition-slow: 0.3s ease;
/* Z-index */
--ui-z-dropdown: 1000;
--ui-z-sticky: 1020;
--ui-z-fixed: 1030;
--ui-z-modal-backdrop: 1040;
--ui-z-modal: 1050;
--ui-z-popover: 1060;
--ui-z-tooltip: 1070;
--ui-z-toast: 1080;
}
/* 深色主题 */
[data-theme="dark"] {
--ui-color-text: rgba(255, 255, 255, 0.85);
--ui-color-text-secondary: rgba(255, 255, 255, 0.65);
--ui-color-text-disabled: rgba(255, 255, 255, 0.25);
--ui-color-border: #434343;
--ui-color-border-hover: #40a9ff;
--ui-color-border-active: #1890ff;
--ui-color-bg: #141414;
--ui-color-bg-secondary: #1f1f1f;
--ui-color-bg-disabled: #262626;
--ui-color-gray-50: #262626;
--ui-color-gray-100: #1f1f1f;
--ui-color-gray-200: #1a1a1a;
--ui-color-gray-300: #434343;
--ui-color-gray-400: #595959;
--ui-color-gray-500: #8c8c8c;
--ui-color-gray-600: #bfbfbf;
--ui-color-gray-700: #d9d9d9;
--ui-color-gray-800: #f0f0f0;
--ui-color-gray-900: #f5f5f5;
}
7. 测试配置
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html']
}
}
})
typescript
// tests/button.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Button from '../packages/components/button/Button.vue'
describe('Button', () => {
it('renders properly', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me'
}
})
expect(wrapper.text()).toContain('Click me')
expect(wrapper.classes()).toContain('ui-button')
})
it('emits click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
})
it('applies correct type class', () => {
const wrapper = mount(Button, {
props: {
type: 'primary'
}
})
expect(wrapper.classes()).toContain('ui-button--primary')
})
it('disables button when disabled prop is true', () => {
const wrapper = mount(Button, {
props: {
disabled: true
}
})
expect(wrapper.attributes('disabled')).toBeDefined()
expect(wrapper.classes()).toContain('ui-button--disabled')
})
it('shows loading state', () => {
const wrapper = mount(Button, {
props: {
loading: true
}
})
expect(wrapper.classes()).toContain('ui-button--loading')
expect(wrapper.find('.ui-button__loading').exists()).toBe(true)
})
})
8. 文档配置
typescript
// docs/.vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'Vue UI Library',
description: '现代化的 Vue 组件库',
themeConfig: {
nav: [
{ text: '指南', link: '/guide/' },
{ text: '组件', link: '/components/' },
{ text: 'GitHub', link: 'https://github.com/your-org/vue-ui-library' }
],
sidebar: {
'/guide/': [
{
text: '开始使用',
items: [
{ text: '介绍', link: '/guide/' },
{ text: '快速开始', link: '/guide/quickstart' },
{ text: '安装', link: '/guide/installation' }
]
}
],
'/components/': [
{
text: '基础组件',
items: [
{ text: 'Button 按钮', link: '/components/button' },
{ text: 'Input 输入框', link: '/components/input' }
]
}
]
}
}
})
9. 构建和发布
json
{
"name": "vue-ui-library",
"version": "1.0.0",
"description": "现代化的 Vue 组件库",
"main": "dist/vue-ui-library.umd.js",
"module": "dist/vue-ui-library.es.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build:docs": "vitepress build docs",
"preview": "vite preview",
"test": "vitest",
"test:coverage": "vitest --coverage",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format": "prettier --write .",
"release": "changeset publish"
},
"peerDependencies": {
"vue": "^3.3.0"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/test-utils": "^2.4.0",
"@vue/tsconfig": "^0.4.0",
"happy-dom": "^10.0.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vitepress": "^1.0.0-rc.4",
"vitest": "^0.34.0",
"vue": "^3.3.4",
"vue-tsc": "^1.8.5"
}
}
最佳实践
1. 组件设计原则
- 单一职责 - 每个组件只负责一个功能
- 可组合性 - 组件可以灵活组合使用
- 一致性 - 保持 API 和视觉风格的一致性
- 可访问性 - 遵循 WCAG 无障碍标准
- 性能优化 - 避免不必要的重渲染
2. API 设计
- 直观的属性名 - 使用清晰易懂的属性名
- 合理的默认值 - 提供常用的默认配置
- 灵活的插槽 - 支持内容定制
- 完整的事件 - 提供必要的事件回调
- 类型安全 - 完整的 TypeScript 类型定义
3. 文档编写
- 清晰的示例 - 提供丰富的使用示例
- 完整的 API - 详细的属性、事件、插槽说明
- 最佳实践 - 分享使用建议和注意事项
- 迁移指南 - 版本升级的迁移说明
部署和发布
1. 自动化构建
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:coverage
- name: Build library
run: npm run build
- name: Build docs
run: npm run build:docs
2. 版本发布
bash
# 创建变更集
npx changeset
# 版本升级
npx changeset version
# 发布到 npm
npx changeset publish
总结
通过本指南,你可以构建一个完整的 Vue 组件库,包含:
- 现代化的组件开发环境
- 完善的类型定义和测试覆盖
- 美观的文档站点
- 自动化的构建和发布流程
这个组件库可以作为团队的基础设施,提高开发效率和代码质量。