Skip to content

TypeScript 最佳实践

本指南涵盖了在 Pinia 中使用 TypeScript 的最佳实践,以创建类型安全、可维护的状态管理解决方案。

项目设置

TypeScript 配置

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": [
    "src/**/*",
    "types/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

Vite 配置

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

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@/stores': resolve(__dirname, 'src/stores'),
      '@/types': resolve(__dirname, 'src/types')
    }
  },
  build: {
    sourcemap: true
  }
})

Store 类型定义

基础 Store 类型

ts
// types/store.ts
export interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
  preferences: UserPreferences
  createdAt: Date
  updatedAt: Date
}

export interface UserPreferences {
  theme: 'light' | 'dark' | 'auto'
  language: string
  notifications: {
    email: boolean
    push: boolean
    sms: boolean
  }
}

export interface UserState {
  currentUser: User | null
  users: User[]
  loading: boolean
  error: string | null
  lastFetch: Date | null
}

export interface UserFilters {
  role?: User['role']
  search?: string
  sortBy?: keyof User
  sortOrder?: 'asc' | 'desc'
}

高级 Store 类型

ts
// types/api.ts
export interface ApiResponse<T> {
  data: T
  message: string
  success: boolean
  timestamp: string
}

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

export interface ApiError {
  code: string
  message: string
  details?: Record<string, unknown>
}

// types/store-actions.ts
export interface AsyncActionState {
  loading: boolean
  error: ApiError | null
  lastExecuted: Date | null
}

export type AsyncActionResult<T> = Promise<{
  success: boolean
  data?: T
  error?: ApiError
}>

Store 实现

Composition API Store

ts
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed, reactive } from 'vue'
import type { 
  User, 
  UserState, 
  UserFilters, 
  AsyncActionResult,
  PaginatedResponse 
} from '@/types'
import { userApi } from '@/api/user'

export const useUserStore = defineStore('user', () => {
  // 状态
  const state = reactive<UserState>({
    currentUser: null,
    users: [],
    loading: false,
    error: null,
    lastFetch: null
  })

  const filters = ref<UserFilters>({})

  // Getters
  const isAuthenticated = computed((): boolean => {
    return state.currentUser !== null
  })

  const isAdmin = computed((): boolean => {
    return state.currentUser?.role === 'admin'
  })

  const filteredUsers = computed((): User[] => {
    let result = state.users

    if (filters.value.role) {
      result = result.filter(user => user.role === filters.value.role)
    }

    if (filters.value.search) {
      const search = filters.value.search.toLowerCase()
      result = result.filter(user => 
        user.name.toLowerCase().includes(search) ||
        user.email.toLowerCase().includes(search)
      )
    }

    if (filters.value.sortBy) {
      result = [...result].sort((a, b) => {
        const aValue = a[filters.value.sortBy!]
        const bValue = b[filters.value.sortBy!]
        
        if (aValue < bValue) return filters.value.sortOrder === 'desc' ? 1 : -1
        if (aValue > bValue) return filters.value.sortOrder === 'desc' ? -1 : 1
        return 0
      })
    }

    return result
  })

  const getUserById = computed(() => {
    return (id: number): User | undefined => {
      return state.users.find(user => user.id === id)
    }
  })

  // Actions
  async function fetchUsers(
    page = 1, 
    limit = 10
  ): AsyncActionResult<PaginatedResponse<User>> {
    state.loading = true
    state.error = null

    try {
      const response = await userApi.getUsers({ page, limit })
      
      if (response.success) {
        state.users = response.data
        state.lastFetch = new Date()
        return { success: true, data: response }
      } else {
        throw new Error(response.message)
      }
    } catch (error) {
      const apiError = error as ApiError
      state.error = apiError.message
      return { success: false, error: apiError }
    } finally {
      state.loading = false
    }
  }

  async function createUser(
    userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>
  ): AsyncActionResult<User> {
    state.loading = true
    state.error = null

    try {
      const response = await userApi.createUser(userData)
      
      if (response.success) {
        state.users.push(response.data)
        return { success: true, data: response.data }
      } else {
        throw new Error(response.message)
      }
    } catch (error) {
      const apiError = error as ApiError
      state.error = apiError.message
      return { success: false, error: apiError }
    } finally {
      state.loading = false
    }
  }

  async function updateUser(
    id: number, 
    updates: Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
  ): AsyncActionResult<User> {
    state.loading = true
    state.error = null

    try {
      const response = await userApi.updateUser(id, updates)
      
      if (response.success) {
        const index = state.users.findIndex(user => user.id === id)
        if (index !== -1) {
          state.users[index] = response.data
        }
        
        if (state.currentUser?.id === id) {
          state.currentUser = response.data
        }
        
        return { success: true, data: response.data }
      } else {
        throw new Error(response.message)
      }
    } catch (error) {
      const apiError = error as ApiError
      state.error = apiError.message
      return { success: false, error: apiError }
    } finally {
      state.loading = false
    }
  }

  async function deleteUser(id: number): AsyncActionResult<void> {
    state.loading = true
    state.error = null

    try {
      const response = await userApi.deleteUser(id)
      
      if (response.success) {
        state.users = state.users.filter(user => user.id !== id)
        
        if (state.currentUser?.id === id) {
          state.currentUser = null
        }
        
        return { success: true }
      } else {
        throw new Error(response.message)
      }
    } catch (error) {
      const apiError = error as ApiError
      state.error = apiError.message
      return { success: false, error: apiError }
    } finally {
      state.loading = false
    }
  }

  function setCurrentUser(user: User | null): void {
    state.currentUser = user
  }

  function setFilters(newFilters: Partial<UserFilters>): void {
    filters.value = { ...filters.value, ...newFilters }
  }

  function clearFilters(): void {
    filters.value = {}
  }

  function clearError(): void {
    state.error = null
  }

  return {
    // 状态
    ...toRefs(state),
    filters: readonly(filters),
    
    // Getters
    isAuthenticated,
    isAdmin,
    filteredUsers,
    getUserById,
    
    // Actions
    fetchUsers,
    createUser,
    updateUser,
    deleteUser,
    setCurrentUser,
    setFilters,
    clearFilters,
    clearError
  }
})

// Store 实例的类型助手
export type UserStore = ReturnType<typeof useUserStore>

Options API Store

ts
// stores/product.ts
import { defineStore } from 'pinia'
import type { Product, ProductState, ProductFilters } from '@/types'
import { productApi } from '@/api/product'

export const useProductStore = defineStore('product', {
  state: (): ProductState => ({
    products: [],
    categories: [],
    selectedProduct: null,
    loading: false,
    error: null,
    filters: {
      category: undefined,
      priceRange: undefined,
      inStock: undefined
    }
  }),

  getters: {
    filteredProducts(): Product[] {
      let result = this.products

      if (this.filters.category) {
        result = result.filter(p => p.category === this.filters.category)
      }

      if (this.filters.priceRange) {
        const [min, max] = this.filters.priceRange
        result = result.filter(p => p.price >= min && p.price <= max)
      }

      if (this.filters.inStock !== undefined) {
        result = result.filter(p => p.inStock === this.filters.inStock)
      }

      return result
    },

    productsByCategory(): Record<string, Product[]> {
      return this.products.reduce((acc, product) => {
        if (!acc[product.category]) {
          acc[product.category] = []
        }
        acc[product.category].push(product)
        return acc
      }, {} as Record<string, Product[]>)
    },

    getProductById(): (id: number) => Product | undefined {
      return (id: number) => this.products.find(p => p.id === id)
    },

    averagePrice(): number {
      if (this.products.length === 0) return 0
      const total = this.products.reduce((sum, p) => sum + p.price, 0)
      return total / this.products.length
    }
  },

  actions: {
    async fetchProducts(): Promise<void> {
      this.loading = true
      this.error = null

      try {
        const response = await productApi.getProducts()
        this.products = response.data
      } catch (error) {
        this.error = (error as Error).message
        throw error
      } finally {
        this.loading = false
      }
    },

    async createProduct(
      productData: Omit<Product, 'id' | 'createdAt' | 'updatedAt'>
    ): Promise<Product> {
      this.loading = true
      this.error = null

      try {
        const response = await productApi.createProduct(productData)
        this.products.push(response.data)
        return response.data
      } catch (error) {
        this.error = (error as Error).message
        throw error
      } finally {
        this.loading = false
      }
    },

    updateFilters(filters: Partial<ProductFilters>): void {
      this.filters = { ...this.filters, ...filters }
    },

    clearFilters(): void {
      this.filters = {
        category: undefined,
        priceRange: undefined,
        inStock: undefined
      }
    }
  }
})

类型安全的 Composables

Store Composables

ts
// composables/useUserActions.ts
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
import type { User, UserFilters } from '@/types'

export function useUserActions() {
  const userStore = useUserStore()

  const actions = {
    async loadUsers(page = 1, limit = 10) {
      const result = await userStore.fetchUsers(page, limit)
      if (!result.success) {
        throw new Error(result.error?.message || '加载用户失败')
      }
      return result.data
    },

    async saveUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) {
      const result = await userStore.createUser(userData)
      if (!result.success) {
        throw new Error(result.error?.message || '创建用户失败')
      }
      return result.data
    },

    async editUser(
      id: number, 
      updates: Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
    ) {
      const result = await userStore.updateUser(id, updates)
      if (!result.success) {
        throw new Error(result.error?.message || '更新用户失败')
      }
      return result.data
    },

    async removeUser(id: number) {
      const result = await userStore.deleteUser(id)
      if (!result.success) {
        throw new Error(result.error?.message || '删除用户失败')
      }
    }
  }

  const filters = {
    setRole(role: User['role'] | undefined) {
      userStore.setFilters({ role })
    },

    setSearch(search: string | undefined) {
      userStore.setFilters({ search })
    },

    setSorting(sortBy: keyof User, sortOrder: 'asc' | 'desc') {
      userStore.setFilters({ sortBy, sortOrder })
    },

    reset() {
      userStore.clearFilters()
    }
  }

  return {
    actions,
    filters,
    // 重新导出具有正确类型的 store 状态和 getters
    users: computed(() => userStore.filteredUsers),
    currentUser: computed(() => userStore.currentUser),
    loading: computed(() => userStore.loading),
    error: computed(() => userStore.error),
    isAuthenticated: computed(() => userStore.isAuthenticated),
    isAdmin: computed(() => userStore.isAdmin)
  }
}

表单 Composables

ts
// composables/useForm.ts
import { ref, reactive, computed } from 'vue'
import type { Ref } from 'vue'

export interface FormField<T> {
  value: T
  error: string | null
  touched: boolean
  required: boolean
}

export interface FormValidationRule<T> {
  message: string
  validator: (value: T) => boolean
}

export interface FormConfig<T extends Record<string, any>> {
  initialValues: T
  validationRules?: {
    [K in keyof T]?: FormValidationRule<T[K]>[]
  }
  onSubmit?: (values: T) => Promise<void> | void
}

export function useForm<T extends Record<string, any>>(
  config: FormConfig<T>
) {
  const fields = reactive(
    Object.keys(config.initialValues).reduce((acc, key) => {
      acc[key as keyof T] = {
        value: config.initialValues[key],
        error: null,
        touched: false,
        required: false
      }
      return acc
    }, {} as { [K in keyof T]: FormField<T[K]> })
  )

  const isSubmitting = ref(false)
  const submitError = ref<string | null>(null)

  const values = computed(() => {
    return Object.keys(fields).reduce((acc, key) => {
      acc[key as keyof T] = fields[key as keyof T].value
      return acc
    }, {} as T)
  })

  const errors = computed(() => {
    return Object.keys(fields).reduce((acc, key) => {
      const field = fields[key as keyof T]
      acc[key as keyof T] = field.error
      return acc
    }, {} as { [K in keyof T]: string | null })
  })

  const isValid = computed(() => {
    return Object.values(errors.value).every(error => error === null)
  })

  const isDirty = computed(() => {
    return Object.keys(fields).some(key => {
      const field = fields[key as keyof T]
      return field.value !== config.initialValues[key as keyof T]
    })
  })

  function validateField<K extends keyof T>(fieldName: K): void {
    const field = fields[fieldName]
    const rules = config.validationRules?.[fieldName] || []
    
    field.error = null
    
    for (const rule of rules) {
      if (!rule.validator(field.value)) {
        field.error = rule.message
        break
      }
    }
  }

  function validateAll(): boolean {
    Object.keys(fields).forEach(key => {
      validateField(key as keyof T)
    })
    return isValid.value
  }

  function setFieldValue<K extends keyof T>(fieldName: K, value: T[K]): void {
    fields[fieldName].value = value
    fields[fieldName].touched = true
    validateField(fieldName)
  }

  function setFieldError<K extends keyof T>(
    fieldName: K, 
    error: string | null
  ): void {
    fields[fieldName].error = error
  }

  function touchField<K extends keyof T>(fieldName: K): void {
    fields[fieldName].touched = true
    validateField(fieldName)
  }

  function reset(): void {
    Object.keys(fields).forEach(key => {
      const field = fields[key as keyof T]
      field.value = config.initialValues[key as keyof T]
      field.error = null
      field.touched = false
    })
    submitError.value = null
  }

  async function submit(): Promise<void> {
    if (!validateAll()) {
      return
    }

    if (!config.onSubmit) {
      return
    }

    isSubmitting.value = true
    submitError.value = null

    try {
      await config.onSubmit(values.value)
    } catch (error) {
      submitError.value = (error as Error).message
      throw error
    } finally {
      isSubmitting.value = false
    }
  }

  return {
    fields,
    values,
    errors,
    isValid,
    isDirty,
    isSubmitting,
    submitError,
    setFieldValue,
    setFieldError,
    touchField,
    validateField,
    validateAll,
    reset,
    submit
  }
}

组件集成

类型安全的组件 Props

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

<script setup lang="ts">
import { computed } from 'vue'
import type { User } from '@/types'
import { useUserStore } from '@/stores/user'

interface Props {
  user: User
  editable?: boolean
  deletable?: boolean
}

interface Emits {
  edit: [user: User]
  delete: [userId: number]
}

const props = withDefaults(defineProps<Props>(), {
  editable: true,
  deletable: true
})

const emit = defineEmits<Emits>()

const userStore = useUserStore()

const roleClass = computed(() => {
  return `role-${props.user.role}`
})

const canEdit = computed(() => {
  return props.editable && (
    userStore.isAdmin || 
    userStore.currentUser?.id === props.user.id
  )
})

const canDelete = computed(() => {
  return props.deletable && 
    userStore.isAdmin && 
    userStore.currentUser?.id !== props.user.id
})

function handleEdit() {
  if (canEdit.value) {
    emit('edit', props.user)
  }
}

function handleDelete() {
  if (canDelete.value) {
    emit('delete', props.user.id)
  }
}
</script>

表单组件

vue
<!-- components/UserForm.vue -->
<template>
  <form @submit.prevent="handleSubmit" class="user-form">
    <div class="form-group">
      <label for="name">姓名 *</label>
      <input
        id="name"
        v-model="form.fields.name.value"
        @blur="form.touchField('name')"
        type="text"
        :class="{ error: form.fields.name.error }"
      />
      <span v-if="form.fields.name.error" class="error-message">
        {{ form.fields.name.error }}
      </span>
    </div>

    <div class="form-group">
      <label for="email">邮箱 *</label>
      <input
        id="email"
        v-model="form.fields.email.value"
        @blur="form.touchField('email')"
        type="email"
        :class="{ error: form.fields.email.error }"
      />
      <span v-if="form.fields.email.error" class="error-message">
        {{ form.fields.email.error }}
      </span>
    </div>

    <div class="form-group">
      <label for="role">角色 *</label>
      <select
        id="role"
        v-model="form.fields.role.value"
        @blur="form.touchField('role')"
        :class="{ error: form.fields.role.error }"
      >
        <option value="">选择角色</option>
        <option value="admin">管理员</option>
        <option value="user">用户</option>
        <option value="guest">访客</option>
      </select>
      <span v-if="form.fields.role.error" class="error-message">
        {{ form.fields.role.error }}
      </span>
    </div>

    <div class="form-actions">
      <button type="button" @click="form.reset()">重置</button>
      <button 
        type="submit" 
        :disabled="!form.isValid || form.isSubmitting"
      >
        {{ form.isSubmitting ? '保存中...' : '保存' }}
      </button>
    </div>

    <div v-if="form.submitError" class="error-message">
      {{ form.submitError }}
    </div>
  </form>
</template>

<script setup lang="ts">
import { useForm } from '@/composables/useForm'
import { useUserActions } from '@/composables/useUserActions'
import type { User } from '@/types'

interface Props {
  initialUser?: Partial<User>
  mode: 'create' | 'edit'
}

interface Emits {
  success: [user: User]
  cancel: []
}

const props = withDefaults(defineProps<Props>(), {
  initialUser: () => ({})
})

const emit = defineEmits<Emits>()

const { actions } = useUserActions()

const form = useForm({
  initialValues: {
    name: props.initialUser.name || '',
    email: props.initialUser.email || '',
    role: props.initialUser.role || '' as User['role']
  },
  validationRules: {
    name: [
      {
        message: '姓名是必填项',
        validator: (value: string) => value.trim().length > 0
      },
      {
        message: '姓名至少需要2个字符',
        validator: (value: string) => value.trim().length >= 2
      }
    ],
    email: [
      {
        message: '邮箱是必填项',
        validator: (value: string) => value.trim().length > 0
      },
      {
        message: '邮箱格式无效',
        validator: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
      }
    ],
    role: [
      {
        message: '角色是必填项',
        validator: (value: string) => ['admin', 'user', 'guest'].includes(value)
      }
    ]
  },
  async onSubmit(values) {
    try {
      let user: User
      
      if (props.mode === 'create') {
        user = await actions.saveUser({
          name: values.name,
          email: values.email,
          role: values.role,
          preferences: {
            theme: 'light',
            language: 'zh',
            notifications: {
              email: true,
              push: true,
              sms: false
            }
          }
        })
      } else {
        user = await actions.editUser(props.initialUser.id!, {
          name: values.name,
          email: values.email,
          role: values.role
        })
      }
      
      emit('success', user)
    } catch (error) {
      // 错误由表单 composable 处理
      throw error
    }
  }
})

function handleSubmit() {
  form.submit()
}
</script>

测试

Store 测试

ts
// tests/stores/user.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
import type { User } from '@/types'
import * as userApi from '@/api/user'

// 模拟 API
vi.mock('@/api/user')
const mockedUserApi = vi.mocked(userApi)

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.clearAllMocks()
  })

  it('应该使用正确的默认状态初始化', () => {
    const store = useUserStore()
    
    expect(store.currentUser).toBeNull()
    expect(store.users).toEqual([])
    expect(store.loading).toBe(false)
    expect(store.error).toBeNull()
    expect(store.isAuthenticated).toBe(false)
    expect(store.isAdmin).toBe(false)
  })

  it('应该成功获取用户', async () => {
    const mockUsers: User[] = [
      {
        id: 1,
        name: '张三',
        email: 'zhangsan@example.com',
        role: 'user',
        preferences: {
          theme: 'light',
          language: 'zh',
          notifications: { email: true, push: true, sms: false }
        },
        createdAt: new Date(),
        updatedAt: new Date()
      }
    ]

    mockedUserApi.getUsers.mockResolvedValue({
      success: true,
      data: mockUsers,
      message: '成功',
      timestamp: new Date().toISOString()
    })

    const store = useUserStore()
    const result = await store.fetchUsers()

    expect(result.success).toBe(true)
    expect(store.users).toEqual(mockUsers)
    expect(store.loading).toBe(false)
    expect(store.error).toBeNull()
  })

  it('应该处理获取用户错误', async () => {
    const errorMessage = '获取用户失败'
    mockedUserApi.getUsers.mockRejectedValue(new Error(errorMessage))

    const store = useUserStore()
    const result = await store.fetchUsers()

    expect(result.success).toBe(false)
    expect(result.error?.message).toBe(errorMessage)
    expect(store.users).toEqual([])
    expect(store.loading).toBe(false)
    expect(store.error).toBe(errorMessage)
  })

  it('应该正确过滤用户', () => {
    const store = useUserStore()
    
    // 设置测试数据
    store.users = [
      {
        id: 1,
        name: '张三',
        email: 'zhangsan@example.com',
        role: 'admin',
        preferences: { theme: 'light', language: 'zh', notifications: { email: true, push: true, sms: false } },
        createdAt: new Date(),
        updatedAt: new Date()
      },
      {
        id: 2,
        name: '李四',
        email: 'lisi@example.com',
        role: 'user',
        preferences: { theme: 'dark', language: 'zh', notifications: { email: true, push: false, sms: false } },
        createdAt: new Date(),
        updatedAt: new Date()
      }
    ]

    // 测试角色过滤
    store.setFilters({ role: 'admin' })
    expect(store.filteredUsers).toHaveLength(1)
    expect(store.filteredUsers[0].name).toBe('张三')

    // 测试搜索过滤
    store.setFilters({ role: undefined, search: '李四' })
    expect(store.filteredUsers).toHaveLength(1)
    expect(store.filteredUsers[0].name).toBe('李四')
  })
})

组件测试

ts
// tests/components/UserCard.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import UserCard from '@/components/UserCard.vue'
import { useUserStore } from '@/stores/user'
import type { User } from '@/types'

const mockUser: User = {
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  role: 'user',
  preferences: {
    theme: 'light',
    language: 'zh',
    notifications: { email: true, push: true, sms: false }
  },
  createdAt: new Date(),
  updatedAt: new Date()
}

describe('UserCard', () => {
  beforeEach(() => {
    // 重置任何模拟
  })

  it('应该正确渲染用户信息', () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser },
      global: {
        plugins: [createTestingPinia()]
      }
    })

    expect(wrapper.find('h3').text()).toBe('张三')
    expect(wrapper.find('p').text()).toBe('zhangsan@example.com')
    expect(wrapper.find('.user-role').text()).toBe('user')
  })

  it('点击编辑按钮时应该发出编辑事件', async () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser, editable: true },
      global: {
        plugins: [createTestingPinia({
          initialState: {
            user: {
              currentUser: { ...mockUser, role: 'admin' },
              isAdmin: true
            }
          }
        })]
      }
    })

    await wrapper.find('button:first-child').trigger('click')
    
    expect(wrapper.emitted('edit')).toBeTruthy()
    expect(wrapper.emitted('edit')![0]).toEqual([mockUser])
  })

  it('当用户无法编辑时应该禁用编辑按钮', () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser, editable: true },
      global: {
        plugins: [createTestingPinia({
          initialState: {
            user: {
              currentUser: { ...mockUser, id: 2, role: 'user' },
              isAdmin: false
            }
          }
        })]
      }
    })

    const editButton = wrapper.find('button:first-child')
    expect(editButton.attributes('disabled')).toBeDefined()
  })
})

性能优化

懒加载 Stores

ts
// stores/index.ts
import type { App } from 'vue'
import { createPinia } from 'pinia'

// 始终需要的核心 stores
import { useUserStore } from './user'
import { useAppStore } from './app'

// 懒加载的 stores
const lazyStores = {
  product: () => import('./product').then(m => m.useProductStore),
  order: () => import('./order').then(m => m.useOrderStore),
  analytics: () => import('./analytics').then(m => m.useAnalyticsStore)
}

export function setupStores(app: App) {
  const pinia = createPinia()
  app.use(pinia)
  
  // 初始化核心 stores
  useUserStore()
  useAppStore()
  
  return pinia
}

export async function useStore<K extends keyof typeof lazyStores>(
  storeName: K
): Promise<ReturnType<Awaited<ReturnType<typeof lazyStores[K]>>>> {
  const storeFactory = await lazyStores[storeName]()
  return storeFactory()
}

// 重新导出核心 stores
export { useUserStore, useAppStore }

计算属性优化

ts
// stores/optimized.ts
import { defineStore } from 'pinia'
import { computed, shallowRef } from 'vue'
import type { Product } from '@/types'

export const useOptimizedStore = defineStore('optimized', () => {
  // 对大数组使用 shallowRef 避免深度响应式
  const products = shallowRef<Product[]>([])
  
  // 记忆化昂贵的计算
  const expensiveComputation = computed(() => {
    // 只有当 products 数组引用改变时才重新计算
    return products.value.reduce((acc, product) => {
      // 这里是昂贵的计算
      return acc + product.price * product.quantity
    }, 0)
  })
  
  // 使用 computed 而不是 options API 中的 getters 来处理派生状态
  const productsByCategory = computed(() => {
    const map = new Map<string, Product[]>()
    
    for (const product of products.value) {
      const category = product.category
      if (!map.has(category)) {
        map.set(category, [])
      }
      map.get(category)!.push(product)
    }
    
    return map
  })
  
  return {
    products,
    expensiveComputation,
    productsByCategory
  }
})

错误处理

全局错误处理器

ts
// utils/errorHandler.ts
import type { ApiError } from '@/types'

export class AppError extends Error {
  constructor(
    message: string,
    public code?: string,
    public statusCode?: number,
    public details?: Record<string, unknown>
  ) {
    super(message)
    this.name = 'AppError'
  }
}

export function handleApiError(error: unknown): ApiError {
  if (error instanceof AppError) {
    return {
      code: error.code || 'UNKNOWN_ERROR',
      message: error.message,
      details: error.details
    }
  }
  
  if (error instanceof Error) {
    return {
      code: 'GENERIC_ERROR',
      message: error.message
    }
  }
  
  return {
    code: 'UNKNOWN_ERROR',
    message: '发生了未知错误'
  }
}

export function createErrorHandler(storeName: string) {
  return (error: unknown): ApiError => {
    const apiError = handleApiError(error)
    
    // 记录错误用于调试
    console.error(`[${storeName}] 错误:`, apiError)
    
    // 报告给错误跟踪服务
    if (import.meta.env.PROD) {
      // reportError(apiError)
    }
    
    return apiError
  }
}

最佳实践总结

1. 类型安全

  • 始终为你的状态、动作和 API 响应定义接口
  • 使用严格的 TypeScript 配置
  • 在可能的地方利用类型推断
  • 创建类型安全的 composables

2. Store 组织

  • 保持 stores 专注和内聚
  • 对复杂逻辑使用 composition API
  • 实现适当的错误处理
  • 使用 async/await 以获得更好的错误传播

3. 性能

  • 对大数据结构使用 shallowRef
  • 为非关键 stores 实现懒加载
  • 优化计算属性
  • 避免不必要的响应式

4. 测试

  • 为 stores 编写全面的单元测试
  • 使用 @pinia/testing 等测试工具
  • 模拟外部依赖
  • 测试成功和错误场景

5. 代码质量

  • 遵循一致的命名约定
  • 记录复杂逻辑
  • 使用 ESLint 和 Prettier
  • 实现适当的错误边界

相关资源

Released under the MIT License.