Store 组合
Pinia 最强大的功能之一是能够将 store 组合在一起。这允许您通过组合多个 store 来创建模块化、可重用和可维护的状态管理解决方案。
基础 Store 组合
在一个 Store 中使用另一个 Store
您可以通过简单地调用另一个 store 来在一个 store 内部使用它:
typescript
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const usePostsStore = defineStore('posts', () => {
const posts = ref([])
const userStore = useUserStore()
async function fetchUserPosts() {
if (!userStore.currentUser) {
throw new Error('用户必须登录')
}
const response = await fetch(`/api/users/${userStore.currentUser.id}/posts`)
posts.value = await response.json()
}
const userPosts = computed(() => {
return posts.value.filter(post => post.authorId === userStore.currentUser?.id)
})
return {
posts: readonly(posts),
userPosts,
fetchUserPosts
}
})
跨 Store 通信
Store 可以通过共享状态和操作相互通信:
typescript
// stores/notifications.ts
export const useNotificationsStore = defineStore('notifications', () => {
const notifications = ref([])
function addNotification(message: string, type: 'success' | 'error' | 'info' = 'info') {
const notification = {
id: Date.now().toString(),
message,
type,
timestamp: new Date()
}
notifications.value.push(notification)
}
function removeNotification(id: string) {
const index = notifications.value.findIndex(n => n.id === id)
if (index > -1) {
notifications.value.splice(index, 1)
}
}
return {
notifications: readonly(notifications),
addNotification,
removeNotification
}
})
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const notificationsStore = useNotificationsStore()
async function login(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
if (response.ok) {
user.value = await response.json()
notificationsStore.addNotification('登录成功!', 'success')
} else {
throw new Error('登录失败')
}
} catch (error) {
notificationsStore.addNotification('登录失败,请重试。', 'error')
throw error
}
}
return { user, login }
})
高级组合模式
Store 工厂模式
为相似功能创建可重用的 store 工厂:
typescript
function createResourceStore<T>(resourceName: string, apiEndpoint: string) {
return defineStore(`${resourceName}Store`, () => {
const items = ref<T[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchAll() {
loading.value = true
error.value = null
try {
const response = await fetch(apiEndpoint)
if (!response.ok) throw new Error(`获取 ${resourceName} 失败`)
items.value = await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : '未知错误'
} finally {
loading.value = false
}
}
async function create(item: Omit<T, 'id'>) {
try {
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
})
if (!response.ok) throw new Error(`创建 ${resourceName} 失败`)
const newItem = await response.json()
items.value.push(newItem)
return newItem
} catch (err) {
error.value = err instanceof Error ? err.message : '未知错误'
throw err
}
}
async function update(id: string, updates: Partial<T>) {
try {
const response = await fetch(`${apiEndpoint}/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
})
if (!response.ok) throw new Error(`更新 ${resourceName} 失败`)
const updatedItem = await response.json()
const index = items.value.findIndex(item => item.id === id)
if (index > -1) {
items.value[index] = updatedItem
}
return updatedItem
} catch (err) {
error.value = err instanceof Error ? err.message : '未知错误'
throw err
}
}
async function remove(id: string) {
try {
const response = await fetch(`${apiEndpoint}/${id}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error(`删除 ${resourceName} 失败`)
const index = items.value.findIndex(item => item.id === id)
if (index > -1) {
items.value.splice(index, 1)
}
} catch (err) {
error.value = err instanceof Error ? err.message : '未知错误'
throw err
}
}
return {
items: readonly(items),
loading: readonly(loading),
error: readonly(error),
fetchAll,
create,
update,
remove
}
})
}
// 使用方法
const useUsersStore = createResourceStore<User>('users', '/api/users')
const usePostsStore = createResourceStore<Post>('posts', '/api/posts')
const useCommentsStore = createResourceStore<Comment>('comments', '/api/comments')
Mixin 模式
创建可以混入到 store 中的可重用功能:
typescript
// mixins/cacheable.ts
function useCacheable<T>(key: string, ttl: number = 5 * 60 * 1000) {
const cache = ref<{ data: T | null; timestamp: number }>({ data: null, timestamp: 0 })
function isCacheValid(): boolean {
return Date.now() - cache.value.timestamp < ttl
}
function setCache(data: T) {
cache.value = {
data,
timestamp: Date.now()
}
}
function getCache(): T | null {
return isCacheValid() ? cache.value.data : null
}
function clearCache() {
cache.value = { data: null, timestamp: 0 }
}
return {
cache: readonly(cache),
isCacheValid,
setCache,
getCache,
clearCache
}
}
// mixins/loadable.ts
function useLoadable() {
const loading = ref(false)
const error = ref<string | null>(null)
async function withLoading<T>(operation: () => Promise<T>): Promise<T> {
loading.value = true
error.value = null
try {
const result = await operation()
return result
} catch (err) {
error.value = err instanceof Error ? err.message : '未知错误'
throw err
} finally {
loading.value = false
}
}
return {
loading: readonly(loading),
error: readonly(error),
withLoading
}
}
// 在 store 中使用 mixins
export const useProductsStore = defineStore('products', () => {
const products = ref<Product[]>([])
const { cache, setCache, getCache, clearCache } = useCacheable<Product[]>('products')
const { loading, error, withLoading } = useLoadable()
async function fetchProducts() {
const cachedProducts = getCache()
if (cachedProducts) {
products.value = cachedProducts
return cachedProducts
}
return withLoading(async () => {
const response = await fetch('/api/products')
if (!response.ok) throw new Error('获取产品失败')
const data = await response.json()
products.value = data
setCache(data)
return data
})
}
function invalidateCache() {
clearCache()
}
return {
products: readonly(products),
loading,
error,
fetchProducts,
invalidateCache
}
})
事件驱动通信
在 store 之间实现事件驱动通信:
typescript
// stores/events.ts
type EventCallback = (...args: any[]) => void
export const useEventBusStore = defineStore('eventBus', () => {
const listeners = ref<Record<string, EventCallback[]>>({})
function on(event: string, callback: EventCallback) {
if (!listeners.value[event]) {
listeners.value[event] = []
}
listeners.value[event].push(callback)
// 返回取消订阅函数
return () => {
const index = listeners.value[event]?.indexOf(callback)
if (index !== undefined && index > -1) {
listeners.value[event].splice(index, 1)
}
}
}
function emit(event: string, ...args: any[]) {
const eventListeners = listeners.value[event]
if (eventListeners) {
eventListeners.forEach(callback => callback(...args))
}
}
function off(event: string, callback?: EventCallback) {
if (!callback) {
delete listeners.value[event]
} else {
const index = listeners.value[event]?.indexOf(callback)
if (index !== undefined && index > -1) {
listeners.value[event].splice(index, 1)
}
}
}
return { on, emit, off }
})
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const eventBus = useEventBusStore()
function addItem(product: Product, quantity: number = 1) {
const existingItem = items.value.find(item => item.productId === product.id)
if (existingItem) {
existingItem.quantity += quantity
} else {
items.value.push({
productId: product.id,
product,
quantity,
addedAt: new Date()
})
}
eventBus.emit('cart:item-added', { product, quantity })
}
function removeItem(productId: string) {
const index = items.value.findIndex(item => item.productId === productId)
if (index > -1) {
const removedItem = items.value.splice(index, 1)[0]
eventBus.emit('cart:item-removed', removedItem)
}
}
return { items, addItem, removeItem }
})
// stores/analytics.ts
export const useAnalyticsStore = defineStore('analytics', () => {
const events = ref<AnalyticsEvent[]>([])
const eventBus = useEventBusStore()
// 监听购物车事件
eventBus.on('cart:item-added', ({ product, quantity }) => {
trackEvent('add_to_cart', {
product_id: product.id,
product_name: product.name,
quantity,
value: product.price * quantity
})
})
eventBus.on('cart:item-removed', (item) => {
trackEvent('remove_from_cart', {
product_id: item.product.id,
product_name: item.product.name,
quantity: item.quantity
})
})
function trackEvent(name: string, properties: Record<string, any>) {
const event = {
id: Date.now().toString(),
name,
properties,
timestamp: new Date()
}
events.value.push(event)
// 发送到分析服务
sendToAnalytics(event)
}
async function sendToAnalytics(event: AnalyticsEvent) {
try {
await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
})
} catch (error) {
console.error('发送分析事件失败:', error)
}
}
return {
events: readonly(events),
trackEvent
}
})
Store 依赖
依赖注入模式
显式管理 store 依赖:
typescript
// stores/dependencies.ts
export interface StoreDependencies {
apiClient: ApiClient
logger: Logger
cache: CacheService
}
const dependencies = ref<StoreDependencies | null>(null)
export function setStoreDependencies(deps: StoreDependencies) {
dependencies.value = deps
}
export function getStoreDependencies(): StoreDependencies {
if (!dependencies.value) {
throw new Error('Store 依赖未初始化')
}
return dependencies.value
}
// stores/products.ts
export const useProductsStore = defineStore('products', () => {
const products = ref<Product[]>([])
const { apiClient, logger, cache } = getStoreDependencies()
async function fetchProducts() {
try {
logger.info('获取产品')
const cached = await cache.get('products')
if (cached) {
products.value = cached
return cached
}
const data = await apiClient.get<Product[]>('/products')
products.value = data
await cache.set('products', data, 300) // 5 分钟
logger.info(`获取了 ${data.length} 个产品`)
return data
} catch (error) {
logger.error('获取产品失败:', error)
throw error
}
}
return {
products: readonly(products),
fetchProducts
}
})
分层 Store 结构
在 store 之间创建分层关系:
typescript
// stores/base.ts
export function createBaseStore(name: string) {
return defineStore(name, () => {
const initialized = ref(false)
const error = ref<string | null>(null)
async function initialize() {
if (initialized.value) return
try {
await onInitialize()
initialized.value = true
} catch (err) {
error.value = err instanceof Error ? err.message : '初始化失败'
throw err
}
}
async function onInitialize() {
// 在子 store 中重写
}
function reset() {
initialized.value = false
error.value = null
}
return {
initialized: readonly(initialized),
error: readonly(error),
initialize,
reset,
onInitialize
}
})
}
// stores/app.ts
export const useAppStore = defineStore('app', () => {
const baseStore = createBaseStore('app')()
const childStores = ref<string[]>([])
async function initializeApp() {
await baseStore.initialize()
// 初始化子 store
for (const storeName of childStores.value) {
const store = getStoreByName(storeName)
if (store && 'initialize' in store) {
await store.initialize()
}
}
}
function registerChildStore(storeName: string) {
if (!childStores.value.includes(storeName)) {
childStores.value.push(storeName)
}
}
return {
...baseStore,
childStores: readonly(childStores),
initializeApp,
registerChildStore
}
})
测试组合的 Store
模拟 Store 依赖
typescript
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from './cart'
import { useNotificationsStore } from './notifications'
describe('购物车 Store 组合', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('添加商品到购物车时应该发送通知', () => {
const cartStore = useCartStore()
const notificationsStore = useNotificationsStore()
const addNotificationSpy = vi.spyOn(notificationsStore, 'addNotification')
const product = { id: '1', name: '测试产品', price: 10 }
cartStore.addItem(product, 2)
expect(addNotificationSpy).toHaveBeenCalledWith(
'已将测试产品添加到购物车',
'success'
)
})
it('应该处理 store 初始化顺序', async () => {
const appStore = useAppStore()
appStore.registerChildStore('cart')
appStore.registerChildStore('notifications')
await appStore.initializeApp()
expect(appStore.initialized).toBe(true)
})
})
最佳实践
1. 避免循环依赖
typescript
// ❌ 错误:循环依赖
// store A 使用 store B,store B 使用 store A
// ✅ 正确:使用共享 store 或事件系统
export const useSharedStore = defineStore('shared', () => {
const sharedState = ref({})
return { sharedState }
})
2. 保持 Store 职责清晰
typescript
// ✅ 正确:每个 store 都有明确的职责
export const useUserStore = defineStore('user', () => {
// 只有用户相关的状态和操作
})
export const useCartStore = defineStore('cart', () => {
// 只有购物车相关的状态和操作
const userStore = useUserStore() // 可以使用其他 store
})
3. 使用 TypeScript 改善组合
typescript
interface StoreComposition {
user: ReturnType<typeof useUserStore>
cart: ReturnType<typeof useCartStore>
notifications: ReturnType<typeof useNotificationsStore>
}
export function useStoreComposition(): StoreComposition {
return {
user: useUserStore(),
cart: useCartStore(),
notifications: useNotificationsStore()
}
}
4. 记录 Store 关系
typescript
/**
* 购物车 Store
*
* 依赖:
* - useUserStore: 用于用户认证状态
* - useProductsStore: 用于产品信息
* - useNotificationsStore: 用于用户反馈
*
* 发出的事件:
* - cart:item-added
* - cart:item-removed
* - cart:cleared
*/
export const useCartStore = defineStore('cart', () => {
// 实现
})
通过遵循这些模式和最佳实践,您可以创建结构良好、可维护的应用程序,其中组合的 Pinia store 可以无缝协作。