Skip to content
On this page

Pinia 最佳实践

本指南将详细介绍在实际项目中使用 Pinia 的最佳实践,涵盖 Store 设计、性能优化、类型安全、测试策略等方面。

Store 设计最佳实践

1. 合理划分 Store

javascript
// 好的做法:按功能模块划分 Store
// stores/user.js
export const useUserStore = defineStore('user', () => {
  const profile = ref(null)
  const permissions = ref([])
  
  const isAuthenticated = computed(() => !!profile.value)
  const isAdmin = computed(() => permissions.value.includes('admin'))
  
  async function fetchProfile() {
    // 获取用户资料
  }
  
  function logout() {
    profile.value = null
    permissions.value = []
  }
  
  return {
    profile,
    permissions,
    isAuthenticated,
    isAdmin,
    fetchProfile,
    logout,
  }
})

// stores/products.js
export const useProductStore = defineStore('products', () => {
  const items = ref([])
  const loading = ref(false)
  
  async function fetchProducts(filters = {}) {
    loading.value = true
    try {
      items.value = await api.getProducts(filters)
    } finally {
      loading.value = false
    }
  }
  
  return {
    items,
    loading,
    fetchProducts,
  }
})

2. 避免 Store 间循环依赖

javascript
// 避免这种循环依赖
// stores/user.js
export const useUserStore = defineStore('user', () => {
  // 错误:直接导入其他 Store
  const productStore = useProductStore() // 不要这样做
})

// 推荐:通过 Actions 参数传递依赖
export const useUserStore = defineStore('user', () => {
  const updateRelatedProducts = (userId, productStore) => {
    // 使用传入的 productStore
    return productStore.updateUserProducts(userId)
  }
  
  return { updateRelatedProducts }
})

3. 使用组合式 API 的 Store

javascript
// 推荐:组合式 API,更灵活的类型推断
import { defineStore } from 'pinia'
import { ref, computed, reactive } from 'vue'

export const useComplexStore = defineStore('complex', () => {
  // 使用 ref 定义响应式状态
  const count = ref(0)
  const items = ref([])
  const metadata = reactive({
    lastUpdated: null,
    version: '1.0.0',
  })
  
  // 计算属性
  const doubleCount = computed(() => count.value * 2)
  const activeItems = computed(() => items.value.filter(item => item.active))
  
  // 方法
  function increment() {
    count.value++
    metadata.lastUpdated = new Date()
  }
  
  function addItem(item) {
    items.value.push({ ...item, id: Date.now(), active: true })
  }
  
  // 返回所有需要暴露的内容
  return {
    // state
    count,
    items,
    metadata,
    
    // getters
    doubleCount,
    activeItems,
    
    // actions
    increment,
    addItem,
  }
})

性能优化策略

1. 合理使用 Getters

javascript
// 避免:在 getter 中进行昂贵的计算
export const useBadPerformanceStore = defineStore('bad-perf', () => {
  const items = ref([])
  
  // 不好:每次访问都进行昂贵计算
  const expensiveCalculation = computed(() => {
    return items.value.map(item => heavyComputation(item)).filter(Boolean)
  })
  
  return { items, expensiveCalculation }
})

// 推荐:缓存昂贵的计算或使用防抖
export const useGoodPerformanceStore = defineStore('good-perf', () => {
  const items = ref([])
  const cachedResult = ref(null)
  const lastUpdated = ref(0)
  
  // 使用防抖优化
  const debouncedCalculation = useDebounceFn(() => {
    cachedResult.value = items.value
      .map(item => heavyComputation(item))
      .filter(Boolean)
    lastUpdated.value = Date.now()
  }, 300)
  
  // 监听 items 变化并触发防抖计算
  watch(items, debouncedCalculation, { deep: true })
  
  return { items, cachedResult }
})

2. 优化大数据量处理

javascript
// 处理大量数据的优化策略
export const useLargeDataStore = defineStore('large-data', () => {
  const allItems = ref([])
  const paginatedItems = ref([])
  const currentPage = ref(1)
  const pageSize = ref(50)
  
  // 使用虚拟滚动或分页的数据
  const visibleItems = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value
    const end = start + pageSize.value
    return allItems.value.slice(start, end)
  })
  
  // 增量更新,而不是替换整个数组
  function addItems(newItems) {
    allItems.value.push(...newItems)
  }
  
  function updateItem(id, updates) {
    const index = allItems.value.findIndex(item => item.id === id)
    if (index !== -1) {
      Object.assign(allItems.value[index], updates)
    }
  }
  
  return {
    allItems,
    paginatedItems,
    visibleItems,
    currentPage,
    pageSize,
    addItems,
    updateItem,
  }
})

3. 状态持久化优化

javascript
// 选择性持久化,避免持久化不必要的状态
export const useOptimizedPersistenceStore = defineStore('optimized-persistence', () => {
  const userSettings = ref({
    theme: 'light',
    language: 'en',
    notifications: true,
  })
  
  const sessionData = ref(null) // 不需要持久化的临时数据
  const cache = ref({}) // 可以重新获取的缓存数据
  
  // 只持久化用户设置
  function $hydrate() {
    const saved = localStorage.getItem('user-settings')
    if (saved) {
      userSettings.value = { ...userSettings.value, ...JSON.parse(saved) }
    }
  }
  
  function $subscribe() {
    // 只在用户设置变化时持久化
    watch(userSettings, (newSettings) => {
      localStorage.setItem('user-settings', JSON.stringify(newSettings))
    }, { deep: true })
  }
  
  return {
    userSettings,
    sessionData,
    cache,
    $hydrate,
    $subscribe,
  }
})

// 或使用 pinia-plugin-persistedstate 的选择性持久化
export const usePluginPersistenceStore = defineStore('plugin-persistence', {
  state: () => ({
    persistentData: {},
    temporaryData: {},
    cache: {},
  }),
  
  persist: {
    pick: ['persistentData'], // 只持久化指定字段
  },
})

TypeScript 最佳实践

1. 定义清晰的类型接口

typescript
// types/index.ts
export interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user' | 'moderator'
  createdAt: Date
  preferences: UserPreferences
}

export interface UserPreferences {
  theme: 'light' | 'dark'
  language: string
  notifications: boolean
}

export interface Product {
  id: number
  name: string
  price: number
  category: string
  inStock: boolean
}

// stores/user.ts
import type { User, UserPreferences } from '@/types'

export const useUserStore = defineStore('user', () => {
  const currentUser = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  const isAuthenticated = computed(() => !!currentUser.value)
  const userRole = computed(() => currentUser.value?.role || 'guest')
  
  async function login(credentials: { email: string; password: string }) {
    loading.value = true
    try {
      const response = await api.login(credentials)
      currentUser.value = response.user
    } catch (err) {
      error.value = (err as Error).message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  function updatePreferences(preferences: Partial<UserPreferences>) {
    if (currentUser.value) {
      currentUser.value.preferences = {
        ...currentUser.value.preferences,
        ...preferences,
      }
    }
  }
  
  return {
    currentUser,
    loading,
    error,
    isAuthenticated,
    userRole,
    login,
    updatePreferences,
  }
})

2. 泛型 Store 模式

typescript
// 通用 CRUD Store 模板
interface CrudStoreOptions<T> {
  resource: string
  defaultItem?: Partial<T>
}

export function useCrudStore<T extends { id: string | number }>(
  id: string,
  options: CrudStoreOptions<T>
) {
  return defineStore(id, () => {
    const items = ref<T[]>([])
    const loading = ref(false)
    const error = ref<string | null>(null)
    const currentItem = ref<T | null>(null)
    
    async function fetchAll() {
      loading.value = true
      try {
        items.value = await api.get(options.resource)
      } catch (err) {
        error.value = (err as Error).message
        throw err
      } finally {
        loading.value = false
      }
    }
    
    async function fetchById(id: T['id']) {
      loading.value = true
      try {
        currentItem.value = await api.get(`${options.resource}/${id}`)
      } catch (err) {
        error.value = (err as Error).message
        throw err
      } finally {
        loading.value = false
      }
    }
    
    async function create(item: Omit<T, 'id'>) {
      try {
        const newItem = await api.post(options.resource, item)
        items.value.push(newItem as T)
        return newItem
      } catch (err) {
        error.value = (err as Error).message
        throw err
      }
    }
    
    async function update(id: T['id'], updates: Partial<T>) {
      try {
        const updatedItem = await api.put(`${options.resource}/${id}`, updates)
        const index = items.value.findIndex(item => item.id === id)
        if (index !== -1) {
          items.value[index] = updatedItem as T
        }
        return updatedItem
      } catch (err) {
        error.value = (err as Error).message
        throw err
      }
    }
    
    async function remove(id: T['id']) {
      try {
        await api.delete(`${options.resource}/${id}`)
        items.value = items.value.filter(item => item.id !== id)
      } catch (err) {
        error.value = (err as Error).message
        throw err
      }
    }
    
    return {
      items,
      loading,
      error,
      currentItem,
      fetchAll,
      fetchById,
      create,
      update,
      remove,
    }
  })
}

// 使用泛型 Store
export const useProductStore = useCrudStore<Product>('products', {
  resource: '/products',
})

测试最佳实践

1. 创建测试辅助函数

typescript
// tests/utils/pinia.ts
import { createTestingPinia, TestingOptions } from '@pinia/testing'
import { setActivePinia, createPinia } from 'pinia'
import { vi } from 'vitest'

export function createTestPinia(options: TestingOptions = {}) {
  const pinia = createTestingPinia({
    createSpy: vi.fn,
    stubActions: false,
    ...options,
  })
  
  setActivePinia(pinia)
  return pinia
}

// 重置所有 Store 的辅助函数
export function resetAllStores() {
  const pinia = createTestingPinia()
  const stores = (pinia.state.value as any) || {}
  
  Object.keys(stores).forEach(storeId => {
    const store = stores[storeId]
    Object.keys(store).forEach(key => {
      store[key] = undefined
    })
  })
}

2. 测试异步操作

typescript
// tests/stores/user.spec.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createTestPinia } from '../utils/pinia'
import { useUserStore } from '@/stores/user'

describe('User Store', () => {
  let store: ReturnType<typeof useUserStore>
  
  beforeEach(() => {
    createTestPinia()
    store = useUserStore()
  })
  
  it('正确处理登录成功', async () => {
    const mockUser = { id: '1', name: 'John', email: 'john@example.com' }
    const loginSpy = vi.spyOn(api, 'login').mockResolvedValue({ user: mockUser })
    
    await store.login({ email: 'john@example.com', password: 'password' })
    
    expect(store.currentUser).toEqual(mockUser)
    expect(store.isAuthenticated).toBe(true)
    expect(loginSpy).toHaveBeenCalledWith({ email: 'john@example.com', password: 'password' })
  })
  
  it('正确处理登录失败', async () => {
    const error = new Error('Invalid credentials')
    vi.spyOn(api, 'login').mockRejectedValue(error)
    
    await expect(store.login({ email: 'invalid', password: 'invalid' }))
      .rejects.toThrow('Invalid credentials')
    
    expect(store.error).toBe('Invalid credentials')
    expect(store.currentUser).toBeNull()
  })
})

安全最佳实践

1. 敏感数据处理

javascript
// 安全的认证 Store
export const useSecureAuthStore = defineStore('secure-auth', () => {
  const user = ref(null)
  const accessToken = ref(null) // 内存中存储,不持久化
  const refreshToken = ref(null) // 内存中存储,不持久化
  const isAuthenticated = computed(() => !!accessToken.value)
  
  // 不要持久化敏感令牌
  const persistConfig = {
    pick: ['user'], // 只持久化用户信息,不包括令牌
  }
  
  async function login(credentials) {
    const response = await api.login(credentials)
    
    // 只在内存中存储令牌
    accessToken.value = response.accessToken
    refreshToken.value = response.refreshToken
    user.value = response.user
    
    // 设置自动刷新令牌
    scheduleTokenRefresh(response.refreshToken)
  }
  
  function logout() {
    accessToken.value = null
    refreshToken.value = null
    user.value = null
    cancelTokenRefresh()
  }
  
  return {
    user,
    isAuthenticated,
    login,
    logout,
  }
})

2. 数据验证

javascript
// 带有数据验证的 Store
export const useValidatedStore = defineStore('validated', () => {
  const profile = ref({
    name: '',
    email: '',
    age: null,
  })
  
  const errors = ref({})
  
  function validateProfile(data) {
    const newErrors = {}
    
    if (!data.name || data.name.length < 2) {
      newErrors.name = 'Name must be at least 2 characters'
    }
    
    if (!data.email || !isValidEmail(data.email)) {
      newErrors.email = 'Please enter a valid email'
    }
    
    if (data.age && (data.age < 0 || data.age > 150)) {
      newErrors.age = 'Age must be between 0 and 150'
    }
    
    errors.value = newErrors
    return Object.keys(newErrors).length === 0
  }
  
  async function updateProfile(newProfile) {
    if (!validateProfile(newProfile)) {
      throw new Error('Validation failed')
    }
    
    // 更新逻辑
    profile.value = { ...profile.value, ...newProfile }
    await api.updateProfile(newProfile)
  }
  
  return {
    profile,
    errors,
    updateProfile,
  }
})

项目结构最佳实践

1. 按功能组织 Store

src/
├── stores/
│   ├── index.ts          # 导出所有 Store
│   ├── modules/
│   │   ├── auth.ts       # 认证相关
│   │   ├── user.ts       # 用户相关
│   │   ├── products.ts   # 产品相关
│   │   └── ui.ts         # UI 状态相关
│   └── types.ts          # 全局类型定义

2. Store 工厂函数

javascript
// stores/factories.ts
export function createStoreFactory<T>(resource: string, defaultState: T) {
  return (id: string) => {
    return defineStore(id, () => {
      const state = ref({ ...defaultState })
      const loading = ref(false)
      const error = ref(null)
      
      async function fetch() {
        loading.value = true
        try {
          const data = await api.get(`/${resource}`)
          Object.assign(state.value, data)
        } catch (err) {
          error.value = (err as Error).message
        } finally {
          loading.value = false
        }
      }
      
      return {
        ...toRefs(state),
        loading,
        error,
        fetch,
      }
    })
  }
}

// 使用工厂函数
const createUserStore = createStoreFactory('user', { name: '', email: '' })
export const useUserStore = createUserStore('user')

遵循这些最佳实践可以帮助您构建可维护、高性能和安全的 Pinia 应用状态管理。