Appearance
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 应用状态管理。