Skip to content

Vite + TypeScript + Pinia 集成

全面指南:如何使用 Vite 和 TypeScript 设置和使用 Pinia,包含高级类型安全、开发工具和生产优化。

功能特性

  • ⚡ 使用 Vite 进行闪电般快速的开发
  • 🔷 完整的 TypeScript 支持和严格类型检查
  • 🎯 高级类型推断和安全性
  • 🛠️ 开发工具和调试
  • 📦 优化的生产构建
  • 🔧 Pinia 自定义 Vite 插件
  • 🎨 代码生成和脚手架
  • 🧪 使用 Vitest 的测试设置
  • 📊 包分析和优化
  • 🔄 热模块替换 (HMR)

项目设置

1. 初始化项目

bash
# 使用 TypeScript 创建 Vite 项目
npm create vite@latest my-pinia-app -- --template vue-ts
cd my-pinia-app

# 安装依赖
npm install
npm install pinia
npm install -D @types/node

2. Vite 配置

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'

// 用于开发的自定义 Pinia 插件
function piniaDevtools() {
  return {
    name: 'pinia-devtools',
    configureServer(server: any) {
      server.middlewares.use('/api/pinia', (req: any, res: any, next: any) => {
        if (req.method === 'GET' && req.url === '/api/pinia/stores') {
          res.setHeader('Content-Type', 'application/json')
          res.end(JSON.stringify({ stores: [] }))
        } else {
          next()
        }
      })
    }
  }
}

export default defineConfig({
  plugins: [
    vue(),
    piniaDevtools(),
    // 包分析器(仅在分析模式下)
    process.env.ANALYZE && visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
      brotliSize: true
    })
  ].filter(Boolean),
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@stores': resolve(__dirname, 'src/stores'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@types': resolve(__dirname, 'src/types')
    }
  },
  
  // TypeScript 配置
  esbuild: {
    target: 'es2020',
    keepNames: true
  },
  
  // 开发服务器
  server: {
    port: 3000,
    open: true,
    cors: true,
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false
      }
    }
  },
  
  // 构建配置
  build: {
    target: 'es2020',
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: true,
    minify: 'esbuild',
    
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'pinia'],
          utils: ['lodash-es', 'date-fns']
        }
      }
    },
    
    // 块大小警告限制
    chunkSizeWarningLimit: 1000
  },
  
  // 优化
  optimizeDeps: {
    include: ['vue', 'pinia', 'lodash-es'],
    exclude: ['@vueuse/core']
  }
})

3. TypeScript 配置

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    
    /* 打包器模式 */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    
    /* 路径映射 */
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@stores/*": ["src/stores/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@types/*": ["src/types/*"]
    },
    
    /* 类型检查 */
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
  ],
  "references": [
    { "path": "./tsconfig.node.json" }
  ]
}

高级类型定义

核心类型

typescript
// src/types/index.ts
export interface User {
  readonly id: string
  email: string
  firstName: string
  lastName: string
  avatar?: string
  roles: readonly Role[]
  preferences: UserPreferences
  createdAt: Date
  updatedAt: Date
}

export interface Role {
  readonly id: string
  name: string
  permissions: readonly Permission[]
}

export interface Permission {
  readonly id: string
  resource: string
  action: 'create' | 'read' | 'update' | 'delete'
}

export interface UserPreferences {
  theme: 'light' | 'dark' | 'auto'
  language: 'en' | 'es' | 'fr' | 'de' | 'zh'
  notifications: {
    email: boolean
    push: boolean
    sms: boolean
  }
  privacy: {
    profileVisibility: 'public' | 'private' | 'friends'
    activityTracking: boolean
  }
}

// API 响应类型
export interface ApiResponse<T> {
  data: T
  message: string
  success: boolean
  timestamp: Date
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number
    limit: number
    total: number
    totalPages: number
    hasNext: boolean
    hasPrev: boolean
  }
}

// 错误类型
export interface ApiError {
  code: string
  message: string
  details?: Record<string, unknown>
  timestamp: Date
}

export interface ValidationError extends ApiError {
  field: string
  value: unknown
  constraints: string[]
}

// Store 状态类型
export interface LoadingState {
  isLoading: boolean
  loadingMessage?: string
}

export interface ErrorState {
  error: ApiError | null
  hasError: boolean
}

export interface CacheState<T> {
  data: T | null
  lastFetched: Date | null
  isStale: boolean
}

// 工具类型
export type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>

// Store 操作类型
export type AsyncAction<T extends any[], R> = (
  ...args: T
) => Promise<R>

export type SyncAction<T extends any[], R> = (
  ...args: T
) => R

Store 类型助手

typescript
// src/types/store.ts
import type { Ref, ComputedRef } from 'vue'
import type { StoreDefinition } from 'pinia'

// 通用 store 状态接口
export interface BaseStoreState {
  loading: boolean
  error: string | null
  lastUpdated: Date | null
}

// Store getters 类型助手
export type StoreGetters<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any
    ? ComputedRef<ReturnType<T[K]>>
    : ComputedRef<T[K]>
}

// Store actions 类型助手
export type StoreActions<T> = {
  [K in keyof T]: T[K] extends (...args: infer P) => infer R
    ? (...args: P) => R
    : never
}

// 类型化 store 定义
export interface TypedStore<
  Id extends string,
  State extends Record<string, any>,
  Getters extends Record<string, any>,
  Actions extends Record<string, any>
> {
  $id: Id
  $state: State
  $getters: StoreGetters<Getters>
  $actions: StoreActions<Actions>
  $patch: (partialState: Partial<State>) => void
  $reset: () => void
  $subscribe: (
    callback: (mutation: any, state: State) => void,
    options?: { detached?: boolean }
  ) => () => void
}

// Store 组合助手
export type StoreComposition<T> = T extends StoreDefinition<
  infer Id,
  infer State,
  infer Getters,
  infer Actions
>
  ? TypedStore<Id, State, Getters, Actions>
  : never

// 异步状态包装器
export interface AsyncState<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<string | null>
  execute: () => Promise<T>
  refresh: () => Promise<T>
  reset: () => void
}

高级 Store 实现

完整 TypeScript 用户 Store

typescript
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed, readonly } from 'vue'
import type {
  User,
  UserPreferences,
  ApiResponse,
  AsyncAction,
  LoadingState,
  ErrorState
} from '@types'
import { userApi } from '@/api/user'
import { validateEmail } from '@utils/validation'

interface LoginCredentials {
  email: string
  password: string
  rememberMe?: boolean
}

interface UpdateProfileData {
  firstName?: string
  lastName?: string
  avatar?: File | string
}

interface UserStoreState extends LoadingState, ErrorState {
  user: User | null
  isAuthenticated: boolean
  sessionExpiry: Date | null
}

export const useUserStore = defineStore('user', () => {
  // 状态
  const state = ref<UserStoreState>({
    user: null,
    isAuthenticated: false,
    sessionExpiry: null,
    isLoading: false,
    error: null,
    hasError: false
  })
  
  // 具有正确类型的计算属性
  const userProfile = computed(() => {
    if (!state.value.user) return null
    
    const { firstName, lastName, email, avatar } = state.value.user
    return {
      fullName: `${firstName} ${lastName}`,
      initials: `${firstName[0]}${lastName[0]}`.toUpperCase(),
      email,
      avatar,
      displayName: firstName || email.split('@')[0]
    } as const
  })
  
  const hasPermission = computed(() => {
    return (resource: string, action: string): boolean => {
      if (!state.value.user) return false
      
      return state.value.user.roles.some(role =>
        role.permissions.some(permission =>
          permission.resource === resource && permission.action === action
        )
      )
    }
  })
  
  const hasRole = computed(() => {
    return (roleName: string): boolean => {
      if (!state.value.user) return false
      return state.value.user.roles.some(role => role.name === roleName)
    }
  })
  
  const isSessionValid = computed(() => {
    if (!state.value.sessionExpiry) return false
    return new Date() < state.value.sessionExpiry
  })
  
  const preferences = computed(() => {
    return state.value.user?.preferences ?? {
      theme: 'auto' as const,
      language: 'zh' as const,
      notifications: {
        email: true,
        push: true,
        sms: false
      },
      privacy: {
        profileVisibility: 'public' as const,
        activityTracking: true
      }
    }
  })
  
  // 具有正确类型的操作
  const login: AsyncAction<[LoginCredentials], User> = async (credentials) => {
    if (!validateEmail(credentials.email)) {
      throw new Error('邮箱格式无效')
    }
    
    state.value.isLoading = true
    state.value.error = null
    state.value.hasError = false
    
    try {
      const response: ApiResponse<{
        user: User
        token: string
        expiresAt: string
      }> = await userApi.login(credentials)
      
      state.value.user = response.data.user
      state.value.isAuthenticated = true
      state.value.sessionExpiry = new Date(response.data.expiresAt)
      
      // 安全存储 token
      localStorage.setItem('auth_token', response.data.token)
      
      return response.data.user
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : '登录失败'
      state.value.error = errorMessage
      state.value.hasError = true
      throw error
    } finally {
      state.value.isLoading = false
    }
  }
  
  const logout: AsyncAction<[], void> = async () => {
    state.value.isLoading = true
    
    try {
      await userApi.logout()
    } catch (error) {
      console.warn('登出 API 调用失败:', error)
    } finally {
      // 无论 API 调用结果如何都清除状态
      state.value.user = null
      state.value.isAuthenticated = false
      state.value.sessionExpiry = null
      state.value.error = null
      state.value.hasError = false
      state.value.isLoading = false
      
      localStorage.removeItem('auth_token')
    }
  }
  
  const updateProfile: AsyncAction<[UpdateProfileData], User> = async (updates) => {
    if (!state.value.user) {
      throw new Error('没有用户登录')
    }
    
    state.value.isLoading = true
    state.value.error = null
    
    try {
      const response: ApiResponse<User> = await userApi.updateProfile(updates)
      
      state.value.user = {
        ...state.value.user,
        ...response.data,
        updatedAt: new Date()
      }
      
      return response.data
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : '更新失败'
      state.value.error = errorMessage
      state.value.hasError = true
      throw error
    } finally {
      state.value.isLoading = false
    }
  }
  
  const updatePreferences: AsyncAction<[Partial<UserPreferences>], UserPreferences> = async (updates) => {
    if (!state.value.user) {
      throw new Error('没有用户登录')
    }
    
    state.value.isLoading = true
    
    try {
      const newPreferences = {
        ...state.value.user.preferences,
        ...updates
      }
      
      const response: ApiResponse<UserPreferences> = await userApi.updatePreferences(newPreferences)
      
      state.value.user = {
        ...state.value.user,
        preferences: response.data,
        updatedAt: new Date()
      }
      
      return response.data
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : '偏好设置更新失败'
      state.value.error = errorMessage
      state.value.hasError = true
      throw error
    } finally {
      state.value.isLoading = false
    }
  }
  
  const refreshUser: AsyncAction<[], User | null> = async () => {
    if (!state.value.isAuthenticated || !isSessionValid.value) {
      return null
    }
    
    state.value.isLoading = true
    
    try {
      const response: ApiResponse<User> = await userApi.getCurrentUser()
      state.value.user = response.data
      return response.data
    } catch (error) {
      console.error('刷新用户失败:', error)
      await logout()
      return null
    } finally {
      state.value.isLoading = false
    }
  }
  
  const clearError = () => {
    state.value.error = null
    state.value.hasError = false
  }
  
  // 从存储的 token 初始化
  const initializeFromStorage = async () => {
    const token = localStorage.getItem('auth_token')
    if (token) {
      try {
        await refreshUser()
      } catch (error) {
        console.warn('从存储初始化用户失败:', error)
        localStorage.removeItem('auth_token')
      }
    }
  }
  
  return {
    // 状态(只读)
    user: readonly(computed(() => state.value.user)),
    isAuthenticated: readonly(computed(() => state.value.isAuthenticated)),
    isLoading: readonly(computed(() => state.value.isLoading)),
    error: readonly(computed(() => state.value.error)),
    hasError: readonly(computed(() => state.value.hasError)),
    sessionExpiry: readonly(computed(() => state.value.sessionExpiry)),
    
    // 计算属性
    userProfile,
    hasPermission,
    hasRole,
    isSessionValid,
    preferences,
    
    // 操作
    login,
    logout,
    updateProfile,
    updatePreferences,
    refreshUser,
    clearError,
    initializeFromStorage
  }
})

// 导出类型供外部使用
export type UserStore = ReturnType<typeof useUserStore>

具有高级功能的产品 Store

typescript
// src/stores/products.ts
import { defineStore } from 'pinia'
import { ref, computed, readonly } from 'vue'
import type {
  PaginatedResponse,
  AsyncAction,
  CacheState
} from '@types'
import { productsApi } from '@/api/products'
import { debounce } from '@utils/debounce'

interface Product {
  readonly id: string
  name: string
  description: string
  price: number
  category: string
  tags: readonly string[]
  images: readonly string[]
  inStock: boolean
  stockQuantity: number
  rating: number
  reviewCount: number
  createdAt: Date
  updatedAt: Date
}

interface ProductFilters {
  category?: string
  minPrice?: number
  maxPrice?: number
  inStock?: boolean
  tags?: readonly string[]
  search?: string
}

interface ProductsStoreState {
  products: CacheState<readonly Product[]>
  currentProduct: CacheState<Product>
  filters: ProductFilters
  pagination: {
    page: number
    limit: number
    total: number
    totalPages: number
  }
  loading: {
    products: boolean
    currentProduct: boolean
    creating: boolean
    updating: boolean
    deleting: boolean
  }
  errors: {
    products: string | null
    currentProduct: string | null
    mutation: string | null
  }
}

export const useProductsStore = defineStore('products', () => {
  // 状态
  const state = ref<ProductsStoreState>({
    products: {
      data: null,
      lastFetched: null,
      isStale: true
    },
    currentProduct: {
      data: null,
      lastFetched: null,
      isStale: true
    },
    filters: {},
    pagination: {
      page: 1,
      limit: 20,
      total: 0,
      totalPages: 0
    },
    loading: {
      products: false,
      currentProduct: false,
      creating: false,
      updating: false,
      deleting: false
    },
    errors: {
      products: null,
      currentProduct: null,
      mutation: null
    }
  })
  
  // 计算属性
  const products = computed(() => state.value.products.data ?? [])
  
  const currentProduct = computed(() => state.value.currentProduct.data)
  
  const filteredProducts = computed(() => {
    let filtered = products.value
    const { category, minPrice, maxPrice, inStock, tags, search } = state.value.filters
    
    if (category) {
      filtered = filtered.filter(p => p.category === category)
    }
    
    if (minPrice !== undefined) {
      filtered = filtered.filter(p => p.price >= minPrice)
    }
    
    if (maxPrice !== undefined) {
      filtered = filtered.filter(p => p.price <= maxPrice)
    }
    
    if (inStock !== undefined) {
      filtered = filtered.filter(p => p.inStock === inStock)
    }
    
    if (tags && tags.length > 0) {
      filtered = filtered.filter(p => 
        tags.some(tag => p.tags.includes(tag))
      )
    }
    
    if (search) {
      const searchLower = search.toLowerCase()
      filtered = filtered.filter(p => 
        p.name.toLowerCase().includes(searchLower) ||
        p.description.toLowerCase().includes(searchLower)
      )
    }
    
    return filtered
  })
  
  const categories = computed(() => {
    const categorySet = new Set(products.value.map(p => p.category))
    return Array.from(categorySet).sort()
  })
  
  const allTags = computed(() => {
    const tagSet = new Set(products.value.flatMap(p => p.tags))
    return Array.from(tagSet).sort()
  })
  
  const priceRange = computed(() => {
    if (products.value.length === 0) return { min: 0, max: 0 }
    
    const prices = products.value.map(p => p.price)
    return {
      min: Math.min(...prices),
      max: Math.max(...prices)
    }
  })
  
  const isLoading = computed(() => {
    return Object.values(state.value.loading).some(Boolean)
  })
  
  const hasErrors = computed(() => {
    return Object.values(state.value.errors).some(Boolean)
  })
  
  // 操作
  const fetchProducts: AsyncAction<[{ page?: number; refresh?: boolean }?], readonly Product[]> = async (options = {}) => {
    const { page = 1, refresh = false } = options
    
    // 如果可用且不过时,使用缓存
    if (!refresh && state.value.products.data && !state.value.products.isStale) {
      return state.value.products.data
    }
    
    state.value.loading.products = true
    state.value.errors.products = null
    
    try {
      const response: PaginatedResponse<Product> = await productsApi.getProducts({
        page,
        limit: state.value.pagination.limit,
        ...state.value.filters
      })
      
      state.value.products = {
        data: response.data,
        lastFetched: new Date(),
        isStale: false
      }
      
      state.value.pagination = {
        page: response.pagination.page,
        limit: response.pagination.limit,
        total: response.pagination.total,
        totalPages: response.pagination.totalPages
      }
      
      return response.data
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : '获取产品失败'
      state.value.errors.products = errorMessage
      throw error
    } finally {
      state.value.loading.products = false
    }
  }
  
  const fetchProductById: AsyncAction<[string, { refresh?: boolean }?], Product> = async (id, options = {}) => {
    const { refresh = false } = options
    
    // 检查产品是否在缓存中
    const cachedProduct = products.value.find(p => p.id === id)
    if (!refresh && cachedProduct) {
      state.value.currentProduct = {
        data: cachedProduct,
        lastFetched: new Date(),
        isStale: false
      }
      return cachedProduct
    }
    
    state.value.loading.currentProduct = true
    state.value.errors.currentProduct = null
    
    try {
      const response = await productsApi.getProductById(id)
      
      state.value.currentProduct = {
        data: response.data,
        lastFetched: new Date(),
        isStale: false
      }
      
      // 如果产品存在于产品缓存中则更新
      if (state.value.products.data) {
        const index = state.value.products.data.findIndex(p => p.id === id)
        if (index !== -1) {
          state.value.products.data = [
            ...state.value.products.data.slice(0, index),
            response.data,
            ...state.value.products.data.slice(index + 1)
          ]
        }
      }
      
      return response.data
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : '获取产品失败'
      state.value.errors.currentProduct = errorMessage
      throw error
    } finally {
      state.value.loading.currentProduct = false
    }
  }
  
  const setFilters = (filters: Partial<ProductFilters>) => {
    state.value.filters = { ...state.value.filters, ...filters }
    state.value.products.isStale = true
  }
  
  const clearFilters = () => {
    state.value.filters = {}
    state.value.products.isStale = true
  }
  
  const searchProducts = debounce((query: string) => {
    setFilters({ search: query })
    fetchProducts({ refresh: true })
  }, 300)
  
  const invalidateCache = () => {
    state.value.products.isStale = true
    state.value.currentProduct.isStale = true
  }
  
  const clearErrors = () => {
    state.value.errors = {
      products: null,
      currentProduct: null,
      mutation: null
    }
  }
  
  return {
    // 状态(只读)
    products: readonly(products),
    currentProduct: readonly(currentProduct),
    filters: readonly(computed(() => state.value.filters)),
    pagination: readonly(computed(() => state.value.pagination)),
    loading: readonly(computed(() => state.value.loading)),
    errors: readonly(computed(() => state.value.errors)),
    
    // 计算属性
    filteredProducts,
    categories,
    allTags,
    priceRange,
    isLoading,
    hasErrors,
    
    // 操作
    fetchProducts,
    fetchProductById,
    setFilters,
    clearFilters,
    searchProducts,
    invalidateCache,
    clearErrors
  }
})

export type ProductsStore = ReturnType<typeof useProductsStore>

开发工具

用于 Store 生成的自定义 Vite 插件

typescript
// scripts/generate-store.ts
import { writeFileSync, mkdirSync } from 'fs'
import { resolve } from 'path'

interface StoreConfig {
  name: string
  fields: Array<{
    name: string
    type: string
    optional?: boolean
  }>
  actions: string[]
}

function generateStore(config: StoreConfig): string {
  const { name, fields, actions } = config
  const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1)
  
  const interfaceFields = fields
    .map(field => `  ${field.name}${field.optional ? '?' : ''}: ${field.type}`)
    .join('\n')
  
  const stateFields = fields
    .map(field => `    ${field.name}: ${getDefaultValue(field.type)}`)
    .join(',\n')
  
  const actionMethods = actions
    .map(action => generateActionMethod(action))
    .join('\n\n')
  
  return `
import { defineStore } from 'pinia'
import { ref, computed, readonly } from 'vue'

interface ${capitalizedName} {
${interfaceFields}
}

interface ${capitalizedName}StoreState {
  items: ${capitalizedName}[]
  currentItem: ${capitalizedName} | null
  loading: boolean
  error: string | null
}

export const use${capitalizedName}Store = defineStore('${name}', () => {
  const state = ref<${capitalizedName}StoreState>({
${stateFields}
  })
  
  // 计算属性
  const items = computed(() => state.value.items)
  const currentItem = computed(() => state.value.currentItem)
  const isLoading = computed(() => state.value.loading)
  const hasError = computed(() => !!state.value.error)
  
  // 操作
${actionMethods}
  
  return {
    // 状态
    items: readonly(items),
    currentItem: readonly(currentItem),
    isLoading: readonly(isLoading),
    hasError: readonly(hasError),
    error: readonly(computed(() => state.value.error)),
    
    // 操作
    ${actions.join(',\n    ')}
  }
})

export type ${capitalizedName}Store = ReturnType<typeof use${capitalizedName}Store>
`
}

function getDefaultValue(type: string): string {
  switch (type) {
    case 'string': return "''"
    case 'number': return '0'
    case 'boolean': return 'false'
    case 'Date': return 'new Date()'
    default: return 'null'
  }
}

function generateActionMethod(action: string): string {
  return `  const ${action} = async () => {
    state.value.loading = true
    state.value.error = null
    
    try {
      // TODO: 实现 ${action}
      console.log('${action} 被调用')
    } catch (error) {
      state.value.error = error instanceof Error ? error.message : '未知错误'
      throw error
    } finally {
      state.value.loading = false
    }
  }`
}

// CLI 使用
if (import.meta.url === `file://${process.argv[1]}`) {
  const config: StoreConfig = {
    name: process.argv[2] || 'example',
    fields: [
      { name: 'id', type: 'string' },
      { name: 'name', type: 'string' },
      { name: 'createdAt', type: 'Date' }
    ],
    actions: ['fetchItems', 'createItem', 'updateItem', 'deleteItem']
  }
  
  const storeCode = generateStore(config)
  const storePath = resolve(`src/stores/${config.name}.ts`)
  
  mkdirSync('src/stores', { recursive: true })
  writeFileSync(storePath, storeCode)
  
  console.log(`Store 已生成: ${storePath}`)
}

测试设置

Vitest 配置

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*'
      ]
    }
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@stores': resolve(__dirname, 'src/stores'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@types': resolve(__dirname, 'src/types')
    }
  }
})

Store 测试工具

typescript
// src/test/store-utils.ts
import { createPinia, setActivePinia, type Pinia } from 'pinia'
import { beforeEach, afterEach, vi } from 'vitest'

export function setupStoreTest() {
  let pinia: Pinia
  
  beforeEach(() => {
    pinia = createPinia()
    setActivePinia(pinia)
  })
  
  afterEach(() => {
    vi.clearAllMocks()
  })
  
  return {
    get pinia() {
      return pinia
    }
  }
}

export function createMockApi<T extends Record<string, any>>(methods: T): T {
  const mock = {} as T
  
  for (const [key, value] of Object.entries(methods)) {
    if (typeof value === 'function') {
      mock[key as keyof T] = vi.fn(value) as T[keyof T]
    } else {
      mock[key as keyof T] = value
    }
  }
  
  return mock
}

export function waitForStoreAction(fn: () => Promise<any>, timeout = 1000): Promise<void> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(`Store 操作在 ${timeout}ms 后超时`))
    }, timeout)
    
    fn().then(() => {
      clearTimeout(timer)
      resolve()
    }).catch((error) => {
      clearTimeout(timer)
      reject(error)
    })
  })
}

Store 测试

typescript
// src/stores/__tests__/user.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useUserStore } from '@stores/user'
import { setupStoreTest, createMockApi } from '@/test/store-utils'
import type { User, ApiResponse } from '@types'

// 模拟 API
const mockUserApi = createMockApi({
  login: vi.fn(),
  logout: vi.fn(),
  getCurrentUser: vi.fn(),
  updateProfile: vi.fn(),
  updatePreferences: vi.fn()
})

vi.mock('@/api/user', () => ({
  userApi: mockUserApi
}))

describe('用户 Store', () => {
  setupStoreTest()
  
  const mockUser: User = {
    id: '1',
    email: 'test@example.com',
    firstName: 'John',
    lastName: 'Doe',
    roles: [],
    preferences: {
      theme: 'light',
      language: 'zh',
      notifications: {
        email: true,
        push: true,
        sms: false
      },
      privacy: {
        profileVisibility: 'public',
        activityTracking: true
      }
    },
    createdAt: new Date(),
    updatedAt: new Date()
  }
  
  beforeEach(() => {
    localStorage.clear()
  })
  
  it('应该使用默认状态初始化', () => {
    const store = useUserStore()
    
    expect(store.user).toBeNull()
    expect(store.isAuthenticated).toBe(false)
    expect(store.isLoading).toBe(false)
    expect(store.error).toBeNull()
  })
  
  it('应该处理成功登录', async () => {
    const store = useUserStore()
    const mockResponse: ApiResponse<{
      user: User
      token: string
      expiresAt: string
    }> = {
      data: {
        user: mockUser,
        token: 'mock-token',
        expiresAt: new Date(Date.now() + 3600000).toISOString()
      },
      message: '登录成功',
      success: true,
      timestamp: new Date()
    }
    
    mockUserApi.login.mockResolvedValue(mockResponse)
    
    const result = await store.login({
      email: 'test@example.com',
      password: 'password'
    })
    
    expect(result).toEqual(mockUser)
    expect(store.user).toEqual(mockUser)
    expect(store.isAuthenticated).toBe(true)
    expect(store.isLoading).toBe(false)
    expect(localStorage.getItem('auth_token')).toBe('mock-token')
  })
  
  it('应该处理登录失败', async () => {
    const store = useUserStore()
    const error = new Error('无效凭据')
    
    mockUserApi.login.mockRejectedValue(error)
    
    await expect(store.login({
      email: 'test@example.com',
      password: 'wrong-password'
    })).rejects.toThrow('无效凭据')
    
    expect(store.user).toBeNull()
    expect(store.isAuthenticated).toBe(false)
    expect(store.hasError).toBe(true)
    expect(store.error).toBe('无效凭据')
  })
  
  it('应该验证邮箱格式', async () => {
    const store = useUserStore()
    
    await expect(store.login({
      email: 'invalid-email',
      password: 'password'
    })).rejects.toThrow('邮箱格式无效')
  })
  
  it('应该正确计算用户资料', () => {
    const store = useUserStore()
    
    // 模拟已认证状态
    store.login({
      email: 'test@example.com',
      password: 'password'
    })
    
    mockUserApi.login.mockResolvedValue({
      data: {
        user: mockUser,
        token: 'token',
        expiresAt: new Date().toISOString()
      },
      message: '',
      success: true,
      timestamp: new Date()
    })
    
    expect(store.userProfile?.fullName).toBe('John Doe')
    expect(store.userProfile?.initials).toBe('JD')
    expect(store.userProfile?.displayName).toBe('John')
  })
  
  it('应该处理登出', async () => {
    const store = useUserStore()
    
    // 设置初始已认证状态
    localStorage.setItem('auth_token', 'mock-token')
    
    await store.logout()
    
    expect(store.user).toBeNull()
    expect(store.isAuthenticated).toBe(false)
    expect(localStorage.getItem('auth_token')).toBeNull()
    expect(mockUserApi.logout).toHaveBeenCalled()
  })
})

生产优化

包分析脚本

json
// package.json scripts
{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "build:analyze": "ANALYZE=true npm run build",
    "preview": "vite preview",
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "type-check": "vue-tsc --noEmit",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
    "generate:store": "tsx scripts/generate-store.ts"
  }
}

性能监控

typescript
// src/utils/performance.ts
export class StorePerformanceMonitor {
  private static instance: StorePerformanceMonitor
  private metrics: Map<string, number[]> = new Map()
  
  static getInstance(): StorePerformanceMonitor {
    if (!this.instance) {
      this.instance = new StorePerformanceMonitor()
    }
    return this.instance
  }
  
  measureAction<T>(storeName: string, actionName: string, action: () => T): T {
    const start = performance.now()
    
    try {
      const result = action()
      
      if (result instanceof Promise) {
        return result.finally(() => {
          this.recordMetric(storeName, actionName, start)
        }) as T
      } else {
        this.recordMetric(storeName, actionName, start)
        return result
      }
    } catch (error) {
      this.recordMetric(storeName, actionName, start)
      throw error
    }
  }
  
  private recordMetric(storeName: string, actionName: string, startTime: number) {
    const duration = performance.now() - startTime
    const key = `${storeName}.${actionName}`
    
    if (!this.metrics.has(key)) {
      this.metrics.set(key, [])
    }
    
    this.metrics.get(key)!.push(duration)
    
    // 只保留最近 100 次测量
    const measurements = this.metrics.get(key)!
    if (measurements.length > 100) {
      measurements.shift()
    }
    
    // 在开发环境中记录慢操作
    if (import.meta.env.DEV && duration > 100) {
      console.warn(`慢 store 操作: ${key} 耗时 ${duration.toFixed(2)}ms`)
    }
  }
  
  getMetrics(): Record<string, { avg: number; min: number; max: number; count: number }> {
    const result: Record<string, { avg: number; min: number; max: number; count: number }> = {}
    
    for (const [key, measurements] of this.metrics.entries()) {
      const avg = measurements.reduce((a, b) => a + b, 0) / measurements.length
      const min = Math.min(...measurements)
      const max = Math.max(...measurements)
      
      result[key] = { avg, min, max, count: measurements.length }
    }
    
    return result
  }
  
  clearMetrics() {
    this.metrics.clear()
  }
}

这个全面的 Vite + TypeScript + Pinia 设置为构建可扩展的 Vue.js 应用程序提供了坚实的基础,具有出色的开发体验、类型安全和生产就绪的优化。

Released under the MIT License.