Vue 3 + TypeScript + Vite企业级开发最佳实践:从项目搭建到性能优化

FatSpirit
FatSpirit 2026-02-01T17:06:23+08:00
0 0 1

前言

随着前端技术的快速发展,Vue 3作为新一代的前端框架,结合TypeScript和Vite构建工具,已经成为了企业级应用开发的主流选择。本文将系统性地分享在Vue 3生态下进行企业级项目开发的最佳实践,涵盖从项目搭建到性能优化的完整流程。

Vue 3 + TypeScript + Vite技术栈优势

Vue 3的核心特性

Vue 3基于Composition API重构,提供了更灵活的组件逻辑组织方式。相比Vue 2的Options API,Composition API让代码更加模块化和可复用。同时,Vue 3在性能上也有显著提升,包括更小的包体积、更快的渲染速度等。

TypeScript的优势

TypeScript为JavaScript添加了静态类型检查,在大型项目中能够有效减少运行时错误,提高开发效率。对于企业级应用而言,类型安全能够确保团队协作的一致性,降低维护成本。

Vite构建工具的特点

Vite作为新一代构建工具,利用浏览器原生ES模块特性,提供了极快的开发服务器启动速度和热更新体验。相比传统的webpack构建工具,Vite在开发阶段的响应速度提升了数倍。

项目初始化与配置

使用Vite创建Vue 3项目

# 使用npm
npm create vite@latest my-vue-app --template vue-ts

# 使用yarn
yarn create vite my-vue-app --template vue-ts

# 使用pnpm
pnpm create vite my-vue-app --template vue-ts

项目结构规划

my-vue-app/
├── public/
│   └── favicon.ico
├── src/
│   ├── assets/           # 静态资源
│   ├── components/       # 公共组件
│   ├── composables/      # 可复用逻辑
│   ├── views/            # 页面组件
│   ├── router/           # 路由配置
│   ├── store/            # 状态管理
│   ├── services/         # API服务
│   ├── utils/            # 工具函数
│   ├── types/            # 类型定义
│   ├── App.vue
│   └── main.ts
├── tests/
├── .env.*                # 环境变量配置
├── vite.config.ts        # Vite配置
└── tsconfig.json         # TypeScript配置

TypeScript配置优化

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "types": ["vite/client"],
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

组件设计规范

组件结构标准化

<!-- src/components/UserCard.vue -->
<template>
  <div class="user-card">
    <img :src="user.avatar" :alt="user.name" class="avatar" />
    <div class="user-info">
      <h3 class="user-name">{{ user.name }}</h3>
      <p class="user-email">{{ user.email }}</p>
      <div class="user-actions">
        <button @click="handleEdit" class="btn btn-edit">编辑</button>
        <button @click="handleDelete" class="btn btn-delete">删除</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

// 定义props类型
interface User {
  id: number
  name: string
  email: string
  avatar: string
}

const props = defineProps<{
  user: User
}>()

// 定义emit事件
const emit = defineEmits<{
  (e: 'edit', user: User): void
  (e: 'delete', userId: number): void
}>()

// 处理编辑事件
const handleEdit = () => {
  emit('edit', props.user)
}

// 处理删除事件
const handleDelete = () => {
  emit('delete', props.user.id)
}
</script>

<style scoped lang="scss">
.user-card {
  display: flex;
  align-items: center;
  padding: 16px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 16px;
  
  .avatar {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    margin-right: 16px;
  }
  
  .user-info {
    flex: 1;
    
    .user-name {
      margin: 0 0 8px 0;
      font-size: 16px;
      font-weight: bold;
    }
    
    .user-email {
      margin: 0 0 12px 0;
      color: #666;
      font-size: 14px;
    }
    
    .user-actions {
      display: flex;
      gap: 8px;
      
      .btn {
        padding: 6px 12px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        
        &.btn-edit {
          background-color: #007bff;
          color: white;
        }
        
        &.btn-delete {
          background-color: #dc3545;
          color: white;
        }
      }
    }
  }
}
</style>

组件通信最佳实践

<!-- src/components/Modal.vue -->
<template>
  <div v-if="visible" class="modal-overlay">
    <div class="modal-content">
      <div class="modal-header">
        <h3>{{ title }}</h3>
        <button @click="handleClose" class="close-btn">×</button>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <button @click="handleCancel" class="btn btn-secondary">取消</button>
        <button @click="handleConfirm" class="btn btn-primary">确认</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

const props = defineProps<{
  visible: boolean
  title?: string
}>()

const emit = defineEmits<{
  (e: 'close'): void
  (e: 'cancel'): void
  (e: 'confirm'): void
}>()

const handleClose = () => {
  emit('close')
}

const handleCancel = () => {
  emit('cancel')
}

const handleConfirm = () => {
  emit('confirm')
}
</script>

状态管理设计

Pinia状态管理

// src/store/userStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { User } from '@/types/user'

export const useUserStore = defineStore('user', () => {
  // 状态
  const users = ref<User[]>([])
  const currentUser = ref<User | null>(null)
  const loading = ref(false)
  
  // 计算属性
  const userCount = computed(() => users.value.length)
  const isLoggedIn = computed(() => !!currentUser.value)
  
  // 动作
  const fetchUsers = async () => {
    loading.value = true
    try {
      const response = await fetch('/api/users')
      users.value = await response.json()
    } catch (error) {
      console.error('获取用户失败:', error)
    } finally {
      loading.value = false
    }
  }
  
  const setCurrentUser = (user: User | null) => {
    currentUser.value = user
  }
  
  const addUser = (user: User) => {
    users.value.push(user)
  }
  
  const updateUser = (updatedUser: User) => {
    const index = users.value.findIndex(u => u.id === updatedUser.id)
    if (index !== -1) {
      users.value[index] = updatedUser
    }
  }
  
  const deleteUser = (userId: number) => {
    users.value = users.value.filter(user => user.id !== userId)
  }
  
  return {
    users,
    currentUser,
    loading,
    userCount,
    isLoggedIn,
    fetchUsers,
    setCurrentUser,
    addUser,
    updateUser,
    deleteUser
  }
})

复用逻辑封装

// src/composables/useApi.ts
import { ref, reactive } from 'vue'
import type { Ref } from 'vue'

export function useApi<T>() {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  const execute = async (apiCall: () => Promise<T>) => {
    loading.value = true
    error.value = null
    
    try {
      data.value = await apiCall()
    } catch (err) {
      error.value = err as Error
      console.error('API调用失败:', err)
    } finally {
      loading.value = false
    }
  }
  
  const reset = () => {
    data.value = null
    loading.value = false
    error.value = null
  }
  
  return {
    data,
    loading,
    error,
    execute,
    reset
  }
}

// 使用示例
export function useUserList() {
  const { data: users, loading, error, execute } = useApi<User[]>()
  
  const fetchUsers = async () => {
    await execute(async () => {
      const response = await fetch('/api/users')
      return response.json()
    })
  }
  
  return {
    users,
    loading,
    error,
    fetchUsers
  }
}

路由配置与权限控制

路由结构设计

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/store/userStore'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/HomeView.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/LoginView.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/DashboardView.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users',
    name: 'Users',
    component: () => import('@/views/UsersView.vue'),
    meta: { requiresAuth: true, permission: 'user:read' }
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/AdminView.vue'),
    meta: { requiresAuth: true, permission: 'admin:access' }
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  
  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    next('/login')
    return
  }
  
  // 权限检查
  if (to.meta.permission && !userStore.currentUser?.permissions?.includes(to.meta.permission)) {
    next('/unauthorized')
    return
  }
  
  next()
})

export default router

权限控制组件

<!-- src/components/PermissionGuard.vue -->
<template>
  <div v-if="hasPermission">
    <slot></slot>
  </div>
  <div v-else class="permission-denied">
    <p>您没有权限访问此内容</p>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useUserStore } from '@/store/userStore'

const props = defineProps<{
  permission: string
}>()

const userStore = useUserStore()
const hasPermission = computed(() => {
  return userStore.currentUser?.permissions?.includes(props.permission) || false
})
</script>

<style scoped>
.permission-denied {
  padding: 20px;
  text-align: center;
  color: #666;
  background-color: #f5f5f5;
  border-radius: 4px;
}
</style>

API服务封装

请求拦截器设计

// src/services/apiClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/userStore'

class ApiClient {
  private client: AxiosInstance
  
  constructor() {
    this.client = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })
    
    this.setupInterceptors()
  }
  
  private setupInterceptors() {
    // 请求拦截器
    this.client.interceptors.request.use(
      (config) => {
        const userStore = useUserStore()
        const token = userStore.currentUser?.token
        
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.client.interceptors.response.use(
      (response: AxiosResponse) => {
        return response.data
      },
      (error) => {
        if (error.response?.status === 401) {
          const userStore = useUserStore()
          userStore.setCurrentUser(null)
          window.location.href = '/login'
        }
        
        return Promise.reject(error)
      }
    )
  }
  
  get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.get<T>(url, config)
  }
  
  post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post<T>(url, data, config)
  }
  
  put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.put<T>(url, data, config)
  }
  
  delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.delete<T>(url, config)
  }
}

export const apiClient = new ApiClient()

服务层封装

// src/services/userService.ts
import { apiClient } from './apiClient'
import type { User } from '@/types/user'

class UserService {
  async getUsers(): Promise<User[]> {
    return apiClient.get<User[]>('/users')
  }
  
  async getUserById(id: number): Promise<User> {
    return apiClient.get<User>(`/users/${id}`)
  }
  
  async createUser(userData: Omit<User, 'id'>): Promise<User> {
    return apiClient.post<User>('/users', userData)
  }
  
  async updateUser(id: number, userData: Partial<User>): Promise<User> {
    return apiClient.put<User>(`/users/${id}`, userData)
  }
  
  async deleteUser(id: number): Promise<void> {
    await apiClient.delete<void>(`/users/${id}`)
  }
  
  async searchUsers(query: string): Promise<User[]> {
    return apiClient.get<User[]>(`/users/search?q=${query}`)
  }
}

export const userService = new UserService()

TypeScript类型定义

基础类型定义

// src/types/user.ts
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: string
  updatedAt: string
  permissions?: string[]
}

export interface LoginCredentials {
  email: string
  password: string
}

export interface AuthResponse {
  token: string
  user: User
}

export interface PaginatedResponse<T> {
  data: T[]
  pagination: {
    page: number
    limit: number
    total: number
    totalPages: number
  }
}

复杂类型定义

// src/types/form.ts
export type FormField = {
  name: string
  label: string
  type: 'text' | 'email' | 'password' | 'select' | 'textarea' | 'date'
  required?: boolean
  placeholder?: string
  options?: { value: string; label: string }[]
  rules?: (value: any) => boolean | string
}

export type FormState<T> = {
  [K in keyof T]: {
    value: T[K]
    error: string | null
    touched: boolean
  }
}

export type FormErrors<T> = Partial<Record<keyof T, string>>

export interface ValidationResult {
  isValid: boolean
  errors: Record<string, string>
}

性能优化策略

组件懒加载

// src/router/index.ts
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/DashboardView.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/reports',
    name: 'Reports',
    component: () => import('@/views/ReportsView.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/analytics',
    name: 'Analytics',
    component: () => import('@/views/AnalyticsView.vue'),
    meta: { requiresAuth: true }
  }
]

虚拟滚动优化

<!-- src/components/VirtualList.vue -->
<template>
  <div class="virtual-list" ref="containerRef">
    <div 
      class="virtual-list-container"
      :style="{ height: totalHeight + 'px' }"
    >
      <div 
        class="virtual-item"
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ 
          position: 'absolute', 
          top: item.top + 'px',
          height: itemHeight + 'px'
        }"
      >
        <component 
          :is="itemComponent" 
          :data="item.data"
        />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'

const props = defineProps<{
  items: any[]
  itemHeight: number
  itemComponent: any
}>()

const containerRef = ref<HTMLDivElement | null>(null)
const scrollTop = ref(0)

const visibleItemCount = computed(() => {
  if (!containerRef.value) return 0
  return Math.ceil(containerRef.value.clientHeight / props.itemHeight)
})

const totalHeight = computed(() => {
  return props.items.length * props.itemHeight
})

const startIndex = computed(() => {
  return Math.floor(scrollTop.value / props.itemHeight)
})

const endIndex = computed(() => {
  return Math.min(startIndex.value + visibleItemCount.value, props.items.length)
})

const visibleItems = computed(() => {
  const start = startIndex.value
  const end = endIndex.value
  
  return props.items.slice(start, end).map((item, index) => ({
    id: item.id,
    data: item,
    top: (start + index) * props.itemHeight
  }))
})

const handleScroll = () => {
  if (containerRef.value) {
    scrollTop.value = containerRef.value.scrollTop
  }
}

onMounted(() => {
  if (containerRef.value) {
    containerRef.value.addEventListener('scroll', handleScroll)
  }
})

watch(() => props.items, () => {
  // 当数据变化时重置滚动位置
  scrollTop.value = 0
})
</script>

缓存策略

// src/utils/cache.ts
class Cache<T> {
  private cache = new Map<string, { data: T; timestamp: number }>()
  private maxSize: number
  private ttl: number
  
  constructor(maxSize = 100, ttl = 5 * 60 * 1000) {
    this.maxSize = maxSize
    this.ttl = ttl
  }
  
  set(key: string, data: T): void {
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    })
  }
  
  get(key: string): T | null {
    const item = this.cache.get(key)
    
    if (!item) return null
    
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key)
      return null
    }
    
    return item.data
  }
  
  has(key: string): boolean {
    return this.cache.has(key)
  }
  
  clear(): void {
    this.cache.clear()
  }
}

export const memoryCache = new Cache<any>()

构建优化配置

Vite配置优化

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    })
  ],
  resolve: {
    alias: {
      '@': resolve(__dirname, './src')
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia', 'axios'],
          ui: ['element-plus'],
          utils: ['lodash-es', 'dayjs']
        }
      }
    },
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
})

Tree Shaking优化

// src/utils/index.ts
// 按需导入,避免全量引入
export { debounce, throttle } from 'lodash-es'
export { formatCurrency } from './format'
export { validateEmail } from './validation'

// 按需导入组件
export { default as Button } from '@/components/Button.vue'
export { default as Input } from '@/components/Input.vue'

测试策略

单元测试配置

// src/__tests__/userStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useUserStore } from '@/store/userStore'

describe('User Store', () => {
  let store: ReturnType<typeof useUserStore>
  
  beforeEach(() => {
    store = useUserStore()
  })
  
  it('should initialize with empty users and loading false', () => {
    expect(store.users).toHaveLength(0)
    expect(store.loading).toBe(false)
  })
  
  it('should add user correctly', () => {
    const user = { id: 1, name: 'John', email: 'john@example.com' }
    store.addUser(user)
    
    expect(store.users).toHaveLength(1)
    expect(store.users[0]).toEqual(user)
  })
  
  it('should update user correctly', () => {
    const initialUser = { id: 1, name: 'John', email: 'john@example.com' }
    const updatedUser = { id: 1, name: 'Jane', email: 'jane@example.com' }
    
    store.addUser(initialUser)
    store.updateUser(updatedUser)
    
    expect(store.users[0]).toEqual(updatedUser)
  })
})

组件测试

<!-- src/__tests__/UserCard.test.vue -->
<template>
  <div>
    <UserCard 
      :user="testUser" 
      @edit="handleEdit"
      @delete="handleDelete"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import UserCard from '@/components/UserCard.vue'

const testUser = {
  id: 1,
  name: 'John Doe',
  email: 'john@example.com',
  avatar: '/avatar.jpg'
}

const handleEdit = vi.fn()
const handleDelete = vi.fn()

// 测试组件渲染
it('renders user card correctly', () => {
  const { getByText, getByAltText } = render(UserCard, {
    props: {
      user: testUser
    }
  })
  
  expect(getByText('John Doe')).toBeInTheDocument()
  expect(getByText('john@example.com')).toBeInTheDocument()
  expect(getByAltText('John Doe')).toBeInTheDocument()
})

// 测试事件触发
it('emits edit event when edit button is clicked', () => {
  const { getByText } = render(UserCard, {
    props: {
      user: testUser
    }
  })
  
  fireEvent.click(getByText('编辑'))
  expect(handleEdit).toHaveBeenCalledWith(testUser)
})
</script>

部署与CI/CD

构建脚本优化

// package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:preview": "vite build --mode preview",
    "build:analyze": "vite-bundle-visualizer",
    "test": "vitest",
    "test:coverage": "vitest run --coverage",
    "lint": "eslint src --ext .ts,.vue --fix",
    "type-check": "tsc --noEmit"
  }
}

部署配置

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run tests
      run: npm run test
      
    - name: Type checking
      run: npm run type-check
      
    - name: Build
      run: npm run build
      
    - name: Deploy to production
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./dist

总结

Vue 3 + TypeScript + Vite的技术栈为企业级应用开发提供了强大的技术支持。通过合理的项目结构设计、组件规范制定

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000