Skip to content

组件库

使用 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 组件库,包含:

  • 现代化的组件开发环境
  • 完善的类型定义和测试覆盖
  • 美观的文档站点
  • 自动化的构建和发布流程

这个组件库可以作为团队的基础设施,提高开发效率和代码质量。

相关资源

vitepress开发指南