Appearance
Pinia Store模块化
在大型应用中,随着业务复杂度的增加,单一Store会变得难以维护。Pinia提供了优雅的Store模块化方案,让我们能够将应用状态拆分成多个相互协作的Store。本章将详细介绍如何设计、实现和管理模块化的Store架构。
Store模块化基础
基础模块划分
typescript
// stores/index.ts - Store入口文件
import { createPinia, defineStore } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
// 在应用中使用Pinia
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
profile: null as User | null,
loading: false,
error: null as string | null
}),
getters: {
isAuthenticated: (state) => !!state.profile,
isAdmin: (state) => state.profile?.role === 'admin',
displayName: (state) => state.profile?.name || 'Guest'
},
actions: {
async login(credentials: LoginCredentials) {
this.loading = true
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
if (!response.ok) throw new Error('Login failed')
this.profile = await response.json()
} catch (error) {
this.error = (error as Error).message
} finally {
this.loading = false
}
},
logout() {
this.profile = null
}
}
})
// stores/products.ts
export const useProductStore = defineStore('products', {
state: () => ({
items: [] as Product[],
categories: [] as Category[],
loading: false,
error: null as string | null
}),
getters: {
featuredProducts: (state) => state.items.filter(p => p.featured),
productsByCategory: (state) => (categoryId: string) =>
state.items.filter(p => p.categoryId === categoryId),
categoryMap: (state) => new Map(state.categories.map(cat => [cat.id, cat]))
},
actions: {
async loadProducts() {
this.loading = true
try {
const response = await fetch('/api/products')
this.items = await response.json()
} catch (error) {
this.error = (error as Error).message
} finally {
this.loading = false
}
},
async loadCategories() {
try {
const response = await fetch('/api/categories')
this.categories = await response.json()
} catch (error) {
this.error = (error as Error).message
}
}
}
})
// stores/cart.ts
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
discount: 0,
shipping: 0
}),
getters: {
subtotal: (state) =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
total: (state) => Math.max(0, this.subtotal - state.discount + state.shipping)
},
actions: {
addToCart(product: Product, quantity: number = 1) {
const existingItem = this.items.find(item => item.productId === product.id)
if (existingItem) {
existingItem.quantity += quantity
} else {
this.items.push({
productId: product.id,
name: product.name,
price: product.price,
quantity
})
}
},
removeFromCart(productId: string) {
this.items = this.items.filter(item => item.productId !== productId)
},
updateQuantity(productId: string, quantity: number) {
if (quantity <= 0) {
this.removeFromCart(productId)
return
}
const item = this.items.find(i => i.productId === productId)
if (item) {
item.quantity = quantity
}
}
}
})
interface User {
id: string
name: string
email: string
role: 'user' | 'admin'
}
interface LoginCredentials {
email: string
password: string
}
interface Product {
id: string
name: string
price: number
categoryId: string
featured: boolean
}
interface Category {
id: string
name: string
description: string
}
interface CartItem {
productId: string
name: string
price: number
quantity: number
}
在组件中使用多个Store
vue
<template>
<div class="ecommerce-app">
<!-- 用户信息 -->
<header class="app-header">
<div class="user-info">
<span>Welcome, {{ userStore.displayName }}!</span>
<button
v-if="userStore.isAuthenticated"
@click="userStore.logout()"
class="logout-btn"
>
Logout
</button>
<button
v-else
@click="showLogin = true"
class="login-btn"
>
Login
</button>
</div>
<!-- 购物车摘要 -->
<div class="cart-summary">
<router-link to="/cart" class="cart-link">
Cart ({{ cartStore.items.length }}) - ${{ cartStore.total.toFixed(2) }}
</router-link>
</div>
</header>
<!-- 产品展示 -->
<main class="main-content">
<section class="featured-products">
<h2>Featured Products</h2>
<div class="product-grid">
<div
v-for="product in productStore.featuredProducts"
:key="product.id"
class="product-card"
>
<h3>{{ product.name }}</h3>
<p>${{ product.price.toFixed(2) }}</p>
<button
@click="addToCart(product)"
:disabled="!userStore.isAuthenticated"
class="add-to-cart-btn"
>
Add to Cart
</button>
</div>
</div>
</section>
</main>
<!-- 登录模态框 -->
<div v-if="showLogin" class="modal-overlay">
<div class="login-modal">
<h2>Login</h2>
<form @submit.prevent="handleLogin">
<input
v-model="loginForm.email"
type="email"
placeholder="Email"
required
/>
<input
v-model="loginForm.password"
type="password"
placeholder="Password"
required
/>
<button type="submit" :disabled="userStore.loading">
{{ userStore.loading ? 'Logging in...' : 'Login' }}
</button>
<button type="button" @click="showLogin = false">Cancel</button>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useUserStore } from '@/stores/user'
import { useProductStore } from '@/stores/products'
import { useCartStore } from '@/stores/cart'
const userStore = useUserStore()
const productStore = useProductStore()
const cartStore = useCartStore()
const showLogin = ref(false)
const loginForm = reactive({
email: '',
password: ''
})
// 加载产品数据
productStore.loadProducts()
const addToCart = (product) => {
cartStore.addToCart(product)
}
const handleLogin = async () => {
try {
await userStore.login(loginForm)
if (!userStore.error) {
showLogin.value = false
loginForm.email = ''
loginForm.password = ''
}
} catch (error) {
console.error('Login failed:', error)
}
}
</script>
<style scoped>
.ecommerce-app {
min-height: 100vh;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.user-info, .cart-summary {
display: flex;
align-items: center;
gap: 1rem;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.add-to-cart-btn {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-to-cart-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.login-modal {
background: white;
padding: 2rem;
border-radius: 8px;
min-width: 300px;
}
.login-modal input {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
Store间通信
直接访问其他Store
typescript
// stores/orders.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'
export const useOrderStore = defineStore('orders', {
state: () => ({
orders: [] as Order[],
currentOrder: null as Order | null,
loading: false,
error: null as string | null
}),
getters: {
userOrders: (state) => {
const userStore = useUserStore()
if (!userStore.profile) return []
return state.orders.filter(order => order.userId === userStore.profile!.id)
},
orderTotal: (state) => {
if (!state.currentOrder) return 0
return state.currentOrder.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
}
},
actions: {
// 创建订单时访问购物车和用户Store
async createOrder() {
const userStore = useUserStore()
const cartStore = useCartStore()
// 验证用户是否已登录
if (!userStore.isAuthenticated) {
throw new Error('User must be authenticated to create an order')
}
// 验证购物车是否为空
if (cartStore.items.length === 0) {
throw new Error('Cannot create order with empty cart')
}
this.loading = true
try {
// 创建订单对象,使用购物车数据
const orderData: CreateOrderRequest = {
userId: userStore.profile!.id,
items: cartStore.items.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: item.price
})),
total: cartStore.total,
status: 'pending'
}
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userStore.profile!.token}` // 假设有token
},
body: JSON.stringify(orderData)
})
if (!response.ok) throw new Error('Failed to create order')
const newOrder = await response.json()
this.orders.push(newOrder)
this.currentOrder = newOrder
// 清空购物车
cartStore.items = []
return newOrder
} catch (error) {
this.error = (error as Error).message
throw error
} finally {
this.loading = false
}
},
// 获取用户订单
async fetchUserOrders() {
const userStore = useUserStore()
if (!userStore.isAuthenticated) {
throw new Error('User must be authenticated to fetch orders')
}
this.loading = true
try {
const response = await fetch(`/api/users/${userStore.profile!.id}/orders`)
if (!response.ok) throw new Error('Failed to fetch orders')
this.orders = await response.json()
} catch (error) {
this.error = (error as Error).message
} finally {
this.loading = false
}
},
// 订单状态变更
async updateOrderStatus(orderId: string, status: OrderStatus) {
const userStore = useUserStore()
const orderIndex = this.orders.findIndex(o => o.id === orderId)
if (orderIndex === -1) {
throw new Error('Order not found')
}
// 验证用户权限(如果是管理员或者订单拥有者)
const order = this.orders[orderIndex]
if (!userStore.isAdmin && order.userId !== userStore.profile?.id) {
throw new Error('Unauthorized to update this order')
}
this.loading = true
try {
const response = await fetch(`/api/orders/${orderId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userStore.profile?.token}`
},
body: JSON.stringify({ status })
})
if (!response.ok) throw new Error('Failed to update order status')
// 更新本地状态
this.orders[orderIndex] = { ...this.orders[orderIndex], status }
if (this.currentOrder?.id === orderId) {
this.currentOrder = { ...this.currentOrder, status }
}
} catch (error) {
this.error = (error as Error).message
} finally {
this.loading = false
}
}
}
})
interface Order {
id: string
userId: string
items: OrderItem[]
total: number
status: OrderStatus
createdAt: string
updatedAt: string
}
interface OrderItem {
productId: string
quantity: number
price: number
}
interface CreateOrderRequest {
userId: string
items: OrderItem[]
total: number
status: OrderStatus
}
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
Store组合模式
typescript
// stores/dashboard.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useProductStore } from './products'
import { useOrderStore } from './orders'
import { useCartStore } from './cart'
export const useDashboardStore = defineStore('dashboard', {
state: () => ({
loading: false,
error: null as string | null,
lastUpdated: null as number | null
}),
getters: {
// 组合多个Store的数据
dashboardStats: (state) => {
const userStore = useUserStore()
const productStore = useProductStore()
const orderStore = useOrderStore()
const cartStore = useCartStore()
return {
user: {
isLoggedIn: userStore.isAuthenticated,
isAdmin: userStore.isAdmin,
name: userStore.displayName
},
products: {
total: productStore.items.length,
featured: productStore.featuredProducts.length
},
orders: {
total: orderStore.orders.length,
userOrders: userStore.profile ? orderStore.userOrders.length : 0
},
cart: {
items: cartStore.items.length,
total: cartStore.total
}
}
},
// 计算用户活动摘要
userActivitySummary: (state) => {
const userStore = useUserStore()
const orderStore = useOrderStore()
if (!userStore.profile) return null
const userOrders = orderStore.orders.filter(
order => order.userId === userStore.profile!.id
)
const totalSpent = userOrders.reduce(
(sum, order) => sum + order.total,
0
)
const lastOrder = userOrders.length > 0
? userOrders.reduce((latest, order) =>
new Date(order.createdAt) > new Date(latest.createdAt) ? order : latest
)
: null
return {
totalOrders: userOrders.length,
totalSpent,
averageOrderValue: userOrders.length > 0 ? totalSpent / userOrders.length : 0,
lastOrderDate: lastOrder?.createdAt,
favoriteCategories: this.getFavoriteCategories(userOrders)
}
}
},
actions: {
// 同时加载多个Store的数据
async loadDashboardData() {
this.loading = true
this.error = null
try {
const userStore = useUserStore()
const productStore = useProductStore()
const orderStore = useOrderStore()
// 并行加载数据
await Promise.all([
userStore.profile ? Promise.resolve() : userStore.loadUserProfile?.(),
productStore.items.length === 0 ? productStore.loadProducts() : Promise.resolve(),
userStore.profile ? orderStore.fetchUserOrders() : Promise.resolve()
])
this.lastUpdated = Date.now()
} catch (error) {
this.error = (error as Error).message
console.error('Dashboard data loading failed:', error)
} finally {
this.loading = false
}
},
// 从订单数据计算最喜欢的商品类别
getFavoriteCategories(orders: Order[]): string[] {
const categoryCount = new Map<string, number>()
const productStore = useProductStore()
orders.forEach(order => {
order.items.forEach(item => {
const product = productStore.items.find(p => p.id === item.productId)
if (product) {
const category = productStore.categoryMap.get(product.categoryId)?.name
if (category) {
categoryCount.set(category, (categoryCount.get(category) || 0) + 1)
}
}
})
})
// 返回按购买次数排序的类别
return Array.from(categoryCount.entries())
.sort((a, b) => b[1] - a[1])
.map(([category]) => category)
.slice(0, 5) // 只返回前5个
},
// 刷新特定数据
async refreshOrders() {
const orderStore = useOrderStore()
await orderStore.fetchUserOrders()
this.lastUpdated = Date.now()
},
async refreshProducts() {
const productStore = useProductStore()
await productStore.loadProducts()
this.lastUpdated = Date.now()
}
}
})
Namespaced Store
按功能模块组织
typescript
// stores/modules/auth.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('token'),
refreshToken: localStorage.getItem('refreshToken'),
user: null as User | null,
loading: false,
error: null as string | null
}),
getters: {
isAuthenticated: (state) => !!state.token,
hasValidSession: (state) => {
if (!state.token) return false
// 这里可以添加token有效性检查
return true
}
},
actions: {
async login(credentials: LoginCredentials) {
this.loading = true
this.error = null
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
if (!response.ok) {
throw new Error('Invalid credentials')
}
const { user, token, refreshToken } = await response.json()
this.user = user
this.token = token
this.refreshToken = refreshToken
// 保存到本地存储
if (token) localStorage.setItem('token', token)
if (refreshToken) localStorage.setItem('refreshToken', refreshToken)
} catch (error) {
this.error = (error as Error).message
throw error
} finally {
this.loading = false
}
},
async logout() {
this.user = null
this.token = null
this.refreshToken = null
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
},
async refreshToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available')
}
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
})
if (!response.ok) {
throw new Error('Failed to refresh token')
}
const { token, refreshToken: newRefreshToken } = await response.json()
this.token = token
this.refreshToken = newRefreshToken
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', newRefreshToken)
} catch (error) {
this.logout() // Refresh failed, log out user
throw error
}
}
}
})
// stores/modules/products/catalog.ts
import { defineStore } from 'pinia'
export const useCatalogStore = defineStore('catalog', {
state: () => ({
products: [] as Product[],
categories: [] as Category[],
brands: [] as Brand[],
filters: {
category: [] as string[],
brand: [] as string[],
minPrice: 0,
maxPrice: Infinity,
inStock: false
},
loading: false,
error: null as string | null
}),
getters: {
filteredProducts: (state) => {
return state.products.filter(product => {
// 类别过滤
if (state.filters.category.length > 0 &&
!state.filters.category.includes(product.categoryId)) {
return false
}
// 品牌过滤
if (state.filters.brand.length > 0 &&
!state.filters.brand.includes(product.brandId)) {
return false
}
// 价格过滤
if (product.price < state.filters.minPrice ||
product.price > state.filters.maxPrice) {
return false
}
// 库存过滤
if (state.filters.inStock && !product.inStock) {
return false
}
return true
})
},
categoryMap: (state) => new Map(state.categories.map(cat => [cat.id, cat])),
brandMap: (state) => new Map(state.brands.map(brand => [brand.id, brand]))
},
actions: {
async loadCatalog() {
this.loading = true
try {
const [productsRes, categoriesRes, brandsRes] = await Promise.all([
fetch('/api/products'),
fetch('/api/categories'),
fetch('/api/brands')
])
const [products, categories, brands] = await Promise.all([
productsRes.json(),
categoriesRes.json(),
brandsRes.json()
])
this.products = products
this.categories = categories
this.brands = brands
} catch (error) {
this.error = (error as Error).message
} finally {
this.loading = false
}
},
setCategoryFilter(categories: string[]) {
this.filters.category = categories
},
setBrandFilter(brands: string[]) {
this.filters.brand = brands
},
setPriceFilter(min: number, max: number) {
this.filters.minPrice = min
this.filters.maxPrice = max
},
setInStockFilter(inStock: boolean) {
this.filters.inStock = inStock
},
clearFilters() {
this.filters = {
category: [],
brand: [],
minPrice: 0,
maxPrice: Infinity,
inStock: false
}
}
}
})
interface Brand {
id: string
name: string
logo: string
}
// stores/modules/products/inventory.ts
export const useInventoryStore = defineStore('inventory', {
state: () => ({
stockLevels: new Map<string, StockLevel>(),
lowStockThreshold: 10,
loading: false,
error: null as string | null
}),
getters: {
lowStockItems: (state) => {
return Array.from(state.stockLevels.entries())
.filter(([_, stock]) => stock.quantity <= state.lowStockThreshold)
.map(([productId, stock]) => ({ productId, ...stock }))
},
outOfStockItems: (state) => {
return Array.from(state.stockLevels.entries())
.filter(([_, stock]) => stock.quantity === 0)
.map(([productId, stock]) => ({ productId, ...stock }))
}
},
actions: {
async loadStockLevels() {
this.loading = true
try {
const response = await fetch('/api/inventory')
const stockData = await response.json()
this.stockLevels = new Map(stockData.map((item: any) => [item.productId, item]))
} catch (error) {
this.error = (error as Error).message
} finally {
this.loading = false
}
},
async updateStock(productId: string, quantity: number) {
try {
const response = await fetch(`/api/inventory/${productId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity })
})
if (!response.ok) throw new Error('Failed to update stock')
const updatedStock = await response.json()
this.stockLevels.set(productId, updatedStock)
} catch (error) {
this.error = (error as Error).message
throw error
}
},
async adjustStock(productId: string, adjustment: number) {
const currentStock = this.stockLevels.get(productId)
if (!currentStock) {
throw new Error('Product not found in inventory')
}
const newQuantity = Math.max(0, currentStock.quantity + adjustment)
await this.updateStock(productId, newQuantity)
}
}
})
interface StockLevel {
productId: string
quantity: number
reserved: number
lastUpdated: string
}
Store依赖管理
Store依赖注入
typescript
// stores/core/baseStore.ts
import { defineStore, StoreDefinition } from 'pinia'
export abstract class BaseStore<T> {
protected state!: T
constructor(protected readonly storeId: string) {}
// 子类需要实现的抽象方法
abstract getState(): T
abstract initialize(): void
// 公共方法
protected async handleAsyncOperation<T>(
operation: () => Promise<T>,
setError: (error: string) => void
): Promise<T | null> {
try {
return await operation()
} catch (error) {
setError((error as Error).message)
return null
}
}
}
// stores/core/apiService.ts
export class ApiService {
private baseUrl: string
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`)
if (!response.ok) {
throw new Error(`GET ${endpoint} failed: ${response.statusText}`)
}
return response.json()
}
async post<T>(endpoint: string, data: any): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error(`POST ${endpoint} failed: ${response.statusText}`)
}
return response.json()
}
async put<T>(endpoint: string, data: any): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error(`PUT ${endpoint} failed: ${response.statusText}`)
}
return response.json()
}
async delete(endpoint: string): Promise<void> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error(`DELETE ${endpoint} failed: ${response.statusText}`)
}
}
}
// stores/features/analytics.ts
import { defineStore } from 'pinia'
import { ApiService } from '../core/apiService'
export const useAnalyticsStore = defineStore('analytics', {
state: () => ({
events: [] as AnalyticsEvent[],
userBehavior: {} as Record<string, UserBehavior>,
loading: false,
error: null as string | null,
apiService: new ApiService('/api/analytics')
}),
getters: {
userEngagementScore: (state) => (userId: string) => {
const behavior = state.userBehavior[userId]
if (!behavior) return 0
// 计算参与度分数
return (
behavior.pageViews * 0.1 +
behavior.clicks * 0.5 +
behavior.sessions * 2 +
(behavior.avgSessionDuration / 60) * 0.1
)
},
topPerformingPages: (state) => {
const pageViews = new Map<string, number>()
state.events
.filter(e => e.type === 'page_view')
.forEach(e => {
const current = pageViews.get(e.properties?.url) || 0
pageViews.set(e.properties?.url, current + 1)
})
return Array.from(pageViews.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
}
},
actions: {
async trackEvent(event: Omit<AnalyticsEvent, 'timestamp'>) {
const fullEvent: AnalyticsEvent = {
...event,
timestamp: new Date().toISOString()
}
try {
// 添加到本地
this.events.push(fullEvent)
// 发送到服务器(异步,不影响用户体验)
this.apiService.post('/events', fullEvent).catch(error => {
console.error('Failed to send analytics event:', error)
// 可以实现本地队列重试机制
})
} catch (error) {
this.error = (error as Error).message
}
},
async loadUserBehavior(userId: string) {
this.loading = true
try {
const behavior = await this.apiService.get<UserBehavior>(`/users/${userId}/behavior`)
this.userBehavior[userId] = behavior
} catch (error) {
this.error = (error as Error).message
} finally {
this.loading = false
}
},
async getConversionFunnel(stepNames: string[]) {
this.loading = true
try {
const funnelData = await this.apiService.post('/funnel', { steps: stepNames })
return funnelData
} catch (error) {
this.error = (error as Error).message
throw error
} finally {
this.loading = false
}
},
async exportAnalytics(startDate: string, endDate: string) {
try {
const response = await fetch(
`/api/analytics/export?start=${startDate}&end=${endDate}`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${this.getToken()}`
}
}
)
if (!response.ok) throw new Error('Export failed')
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `analytics_${startDate}_to_${endDate}.csv`
a.click()
window.URL.revokeObjectURL(url)
} catch (error) {
this.error = (error as Error).message
}
},
// 辅助方法
getToken(): string {
// 这里应该从认证Store获取token
// 为示例简单返回空字符串
return ''
}
}
})
interface AnalyticsEvent {
id: string
type: string
userId: string
sessionId: string
timestamp: string
properties?: Record<string, any>
}
interface UserBehavior {
userId: string
pageViews: number
clicks: number
sessions: number
avgSessionDuration: number // in seconds
lastActive: string
preferredPages: string[]
}
Store动态注册
动态Store管理
typescript
// stores/manager.ts
import { createPinia, defineStore, Pinia, StoreDefinition } from 'pinia'
import { useUserStore } from './modules/auth'
interface StoreRegistry {
[key: string]: StoreDefinition
}
export class StoreManager {
private static instance: StoreManager
private pinia: Pinia | null = null
private registry: StoreRegistry = {}
private loadedStores: Set<string> = new Set()
private constructor() {}
static getInstance(): StoreManager {
if (!StoreManager.instance) {
StoreManager.instance = new StoreManager()
}
return StoreManager.instance
}
initialize(pinia: Pinia) {
this.pinia = pinia
}
registerStore<T>(storeId: string, storeDefinition: StoreDefinition<T>) {
this.registry[storeId] = storeDefinition
}
async loadStore(storeId: string): Promise<any> {
if (this.loadedStores.has(storeId)) {
// Store already loaded, return existing instance
return this.getStoreInstance(storeId)
}
if (!this.registry[storeId]) {
throw new Error(`Store ${storeId} is not registered`)
}
if (!this.pinia) {
throw new Error('Pinia not initialized')
}
// 动态注册Store
const storeDefinition = this.registry[storeId]
const store = storeDefinition()
this.loadedStores.add(storeId)
return store
}
getStoreInstance(storeId: string) {
if (!this.pinia) {
throw new Error('Pinia not initialized')
}
return (this.pinia as any)._s.get(storeId)
}
unloadStore(storeId: string) {
if (!this.pinia) {
throw new Error('Pinia not initialized')
}
const storeInstance = this.getStoreInstance(storeId)
if (storeInstance) {
// 清理Store实例
storeInstance.$reset()
this.loadedStores.delete(storeId)
}
}
isStoreLoaded(storeId: string): boolean {
return this.loadedStores.has(storeId)
}
}
// 使用Store管理器
export const storeManager = StoreManager.getInstance()
// 动态Store示例
export const useDynamicModuleStore = defineStore('dynamicModule', {
state: () => ({
modules: new Map<string, ModuleConfig>(),
activeModules: [] as string[],
loading: false,
error: null as string | null
}),
getters: {
getModuleConfig: (state) => (moduleId: string) => {
return state.modules.get(moduleId)
},
isActive: (state) => (moduleId: string) => {
return state.activeModules.includes(moduleId)
},
activeModuleConfigs: (state) => {
return state.activeModules
.map(id => state.modules.get(id))
.filter(config => config !== undefined) as ModuleConfig[]
}
},
actions: {
async loadModule(moduleId: string) {
if (this.isActive(moduleId)) {
return // 模块已激活
}
this.loading = true
try {
// 获取模块配置
const response = await fetch(`/api/modules/${moduleId}/config`)
if (!response.ok) throw new Error(`Failed to load module ${moduleId}`)
const config: ModuleConfig = await response.json()
this.modules.set(moduleId, config)
// 动态加载模块相关的Store
await this.loadModuleStores(moduleId, config.requiredStores)
// 激活模块
this.activeModules.push(moduleId)
} catch (error) {
this.error = (error as Error).message
throw error
} finally {
this.loading = false
}
},
async loadModuleStores(moduleId: string, storeIds: string[]) {
for (const storeId of storeIds) {
try {
await storeManager.loadStore(storeId)
} catch (error) {
console.error(`Failed to load store ${storeId} for module ${moduleId}:`, error)
}
}
},
async unloadModule(moduleId: string) {
const moduleIndex = this.activeModules.indexOf(moduleId)
if (moduleIndex === -1) {
return // 模块未激活
}
// 从激活列表中移除
this.activeModules.splice(moduleIndex, 1)
// 卸载模块相关的Store
const config = this.modules.get(moduleId)
if (config) {
config.requiredStores.forEach(storeId => {
storeManager.unloadStore(storeId)
})
}
// 从注册表中移除模块配置
this.modules.delete(moduleId)
},
async toggleModule(moduleId: string) {
if (this.isActive(moduleId)) {
await this.unloadModule(moduleId)
} else {
await this.loadModule(moduleId)
}
}
}
})
interface ModuleConfig {
id: string
name: string
description: string
version: string
requiredStores: string[]
permissions: string[]
dependencies: string[]
}
Store测试策略
模块化Store测试
typescript
// __tests__/module-stores.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/modules/auth'
import { useCatalogStore } from '@/stores/modules/products/catalog'
import { useCartStore } from '@/stores/cart'
import { useOrderStore } from '@/stores/orders'
import { vi, describe, it, beforeEach, expect, afterEach } from 'vitest'
describe('Module Stores Integration', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
afterEach(() => {
vi.restoreAllMocks()
})
it('should coordinate between user, catalog, cart, and order stores', async () => {
const userStore = useUserStore()
const catalogStore = useCatalogStore()
const cartStore = useCartStore()
const orderStore = useOrderStore()
// Mock API responses
const mockUser = { id: 'user1', name: 'John Doe', email: 'john@example.com', role: 'user' }
const mockProducts = [
{ id: 'prod1', name: 'Laptop', price: 1000, categoryId: 'cat1', featured: true },
{ id: 'prod2', name: 'Mouse', price: 25, categoryId: 'cat1', featured: false }
]
global.fetch = vi.fn((input: any) => {
if (input.includes('/api/auth/login')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ user: mockUser, token: 'fake-token' })
} as Response)
}
if (input.includes('/api/products')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(mockProducts)
} as Response)
}
if (input.includes('/api/orders')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ id: 'order1', userId: 'user1', items: [], total: 0, status: 'pending' })
} as Response)
}
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) } as Response)
}) as any
// 1. User logs in
await userStore.login({ email: 'john@example.com', password: 'password' })
expect(userStore.isAuthenticated).toBe(true)
expect(userStore.profile?.name).toBe('John Doe')
// 2. Load products
await catalogStore.loadCatalog()
expect(catalogStore.products.length).toBeGreaterThan(0)
// 3. Add product to cart
const laptop = catalogStore.products[0]
cartStore.addToCart(laptop, 2)
expect(cartStore.items.length).toBe(1)
expect(cartStore.total).toBe(2000) // 2 * $1000
// 4. Create order
const order = await orderStore.createOrder()
expect(order).not.toBeNull()
expect(cartStore.items.length).toBe(0) // Cart should be emptied after order creation
})
it('should handle cross-store dependencies correctly', async () => {
const userStore = useUserStore()
const orderStore = useOrderStore()
// Set up user
userStore.$patch({
profile: { id: 'user1', name: 'Jane Doe', email: 'jane@example.com', role: 'admin' },
token: 'admin-token'
})
// Mock orders for this user
const mockOrders = [
{ id: 'order1', userId: 'user1', items: [], total: 100, status: 'pending', createdAt: '2023-01-01' },
{ id: 'order2', userId: 'user1', items: [], total: 200, status: 'delivered', createdAt: '2023-01-02' }
]
global.fetch = vi.fn((input: any) => {
if (input.includes('/api/users/user1/orders')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(mockOrders)
} as Response)
}
if (input.includes('/api/orders/order1/status')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ ...mockOrders[0], status: 'processing' })
} as Response)
}
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) } as Response)
}) as any
// Fetch user orders
await orderStore.fetchUserOrders()
expect(orderStore.orders.length).toBe(2)
// Check that userOrders getter works correctly
expect(orderStore.userOrders.length).toBe(2)
// Update order status
await orderStore.updateOrderStatus('order1', 'processing')
expect(orderStore.orders[0].status).toBe('processing')
})
it('should maintain data consistency across stores', async () => {
const catalogStore = useCatalogStore()
const cartStore = useCartStore()
const products = [
{ id: 'p1', name: 'Product 1', price: 10, categoryId: 'c1', brandId: 'b1', inStock: true },
{ id: 'p2', name: 'Product 2', price: 20, categoryId: 'c1', brandId: 'b2', inStock: false }
]
catalogStore.$patch({ products })
// Add products to cart
cartStore.addToCart(products[0], 3) // Available product
cartStore.addToCart(products[1], 1) // Out of stock product
// Verify cart total calculation
expect(cartStore.subtotal).toBe(50) // 3*10 + 1*20
// Apply filter
catalogStore.setInStockFilter(true)
// Verify filtered products
expect(catalogStore.filteredProducts.length).toBe(1)
expect(catalogStore.filteredProducts[0].id).toBe('p1')
})
})
Store架构最佳实践
1. Store分层架构
typescript
// stores/layers/presentation.ts
import { defineStore } from 'pinia'
import { useDomainStore } from './domain'
export const usePresentationStore = defineStore('presentation', {
state: () => ({
uiState: {
activeTab: 'dashboard',
sidebarCollapsed: false,
theme: 'light' as 'light' | 'dark',
notifications: [] as Notification[],
modals: [] as Modal[]
}
}),
getters: {
// 格式化数据供UI使用
formattedUserStats: (state) => {
const domainStore = useDomainStore()
return {
...domainStore.userStats,
formattedJoinDate: new Date(domainStore.userStats.joinDate).toLocaleDateString(),
readableActivityLevel: state.getActivityLevelLabel(domainStore.userStats.activityScore)
}
}
},
actions: {
setTab(tab: string) {
this.uiState.activeTab = tab
},
toggleSidebar() {
this.uiState.sidebarCollapsed = !this.uiState.sidebarCollapsed
},
setTheme(theme: 'light' | 'dark') {
this.uiState.theme = theme
document.documentElement.setAttribute('data-theme', theme)
},
addNotification(notification: Omit<Notification, 'id'>) {
this.uiState.notifications.push({
...notification,
id: Date.now().toString()
})
},
removeNotification(id: string) {
this.uiState.notifications = this.uiState.notifications.filter(n => n.id !== id)
},
getActivityLevelLabel(score: number): string {
if (score > 80) return 'Very Active'
if (score > 60) return 'Active'
if (score > 40) return 'Moderate'
return 'Low'
}
}
})
// stores/layers/domain.ts
export const useDomainStore = defineStore('domain', {
state: () => ({
userStats: {
joinDate: new Date().toISOString(),
activityScore: 0,
completedTasks: 0,
pendingTasks: 0,
achievementPoints: 0
},
businessRules: {
maxDailyTasks: 10,
minPasswordLength: 8,
sessionTimeout: 30 * 60 * 1000 // 30 minutes
}
}),
getters: {
canCreateNewTask: (state) => {
return state.userStats.completedTasks + state.userStats.pendingTasks < state.businessRules.maxDailyTasks
},
isSessionExpired: (state) => {
// 这里可以实现会话过期逻辑
return false
}
},
actions: {
updateActivityScore(increment: number) {
this.userStats.activityScore = Math.min(100, this.userStats.activityScore + increment)
},
completeTask() {
this.userStats.completedTasks++
this.userStats.pendingTasks = Math.max(0, this.userStats.pendingTasks - 1)
this.updateActivityScore(5)
},
addPendingTask() {
if (this.canCreateNewTask) {
this.userStats.pendingTasks++
}
}
}
})
// stores/layers/infrastructure.ts
export const useInfrastructureStore = defineStore('infrastructure', {
state: () => ({
api: {
baseUrl: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3000',
timeout: 10000,
retries: 3
},
cache: new Map<string, { data: any; timestamp: number }>(),
offlineQueue: [] as QueuedRequest[],
syncStatus: 'idle' as 'idle' | 'syncing' | 'error'
}),
actions: {
async makeApiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const cacheKey = `${options.method || 'GET'}:${endpoint}`
// 检查缓存
const cached = this.cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5分钟缓存
return cached.data
}
try {
const response = await fetch(`${this.api.baseUrl}${endpoint}`, {
...options,
timeout: this.api.timeout
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
// 缓存结果
this.cache.set(cacheKey, { data, timestamp: Date.now() })
return data
} catch (error) {
// 如果在线,尝试重试
if (navigator.onLine) {
for (let i = 0; i < this.api.retries; i++) {
try {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000))
return await this.makeApiRequest(endpoint, options)
} catch (retryError) {
if (i === this.api.retries - 1) throw retryError
}
}
} else {
// 离线时加入队列
this.offlineQueue.push({ endpoint, options, timestamp: Date.now() })
}
throw error
}
},
async processOfflineQueue() {
if (this.offlineQueue.length === 0 || !navigator.onLine) return
this.syncStatus = 'syncing'
try {
for (const request of [...this.offlineQueue]) {
try {
await this.makeApiRequest(request.endpoint, request.options)
this.offlineQueue.shift()
} catch (error) {
console.error('Failed to sync request:', error)
break // 停止同步直到网络恢复
}
}
} finally {
this.syncStatus = this.offlineQueue.length > 0 ? 'error' : 'idle'
}
}
}
})
interface Notification {
id: string
title: string
message: string
type: 'info' | 'success' | 'warning' | 'error'
timestamp: number
}
interface Modal {
id: string
type: string
props: Record<string, any>
}
interface QueuedRequest {
endpoint: string
options: RequestInit
timestamp: number
}
2. Store关系管理
typescript
// stores/relationships.ts
import { defineStore } from 'pinia'
export const useRelationshipStore = defineStore('relationships', {
state: () => ({
associations: {
user: {
orders: [] as string[], // userId -> orderIds
addresses: [] as string[],
preferences: null as string | null
},
product: {
reviews: [] as string[],
categories: [] as string[],
inventory: null as string | null
},
order: {
items: [] as string[],
user: null as string | null,
payments: [] as string[]
}
},
foreignKeys: new Map<string, Map<string, string>>() // storeName -> id -> foreignId
}),
actions: {
// 定义一对多关系
defineOneToMany(parentStore: string, parentId: string, childStore: string, childIds: string[]) {
const key = `${parentStore}-${parentId}`
if (!this.foreignKeys.has(key)) {
this.foreignKeys.set(key, new Map())
}
const relations = this.foreignKeys.get(key)!
childIds.forEach(childId => {
relations.set(childId, parentId)
})
},
// 定义多对一关系
defineManyToOne(childStore: string, childId: string, parentStore: string, parentId: string) {
const key = `${childStore}-${childId}`
if (!this.foreignKeys.has(key)) {
this.foreignKeys.set(key, new Map())
}
this.foreignKeys.get(key)!.set(childId, parentId)
},
// 获取子项
getChildren(parentStore: string, parentId: string, childStore: string): string[] {
const key = `${parentStore}-${parentId}`
const relations = this.foreignKeys.get(key)
if (!relations) return []
return Array.from(relations.keys()).filter(childId =>
relations.get(childId) === parentId
)
},
// 获取父项
getParent(childStore: string, childId: string, parentStore: string): string | null {
const key = `${childStore}-${childId}`
const relations = this.foreignKeys.get(key)
if (!relations) return null
return relations.get(childId) || null
},
// 级联删除
cascadeDelete(storeName: string, id: string) {
// 查找所有相关的外键关系并删除
this.foreignKeys.forEach((relations, key) => {
if (key.startsWith(storeName)) {
relations.forEach((value, relationId) => {
if (value === id) {
relations.delete(relationId)
}
})
// 如果关系表为空,删除整个条目
if (relations.size === 0) {
this.foreignKeys.delete(key)
}
}
})
}
}
})
总结
Store模块化是构建大型Vue/Pinia应用的关键架构决策。通过本章的学习,你应该掌握:
- 基础模块划分:如何合理地拆分应用状态
- Store间通信:不同Store之间的数据交互方式
- Namespaced Store:按功能或领域组织Store
- 依赖管理:管理Store之间的依赖关系
- 动态注册:按需加载和卸载Store
- 测试策略:模块化Store的测试方法
- 架构最佳实践:分层架构和关系管理
模块化的Store架构提高了代码的可维护性、可测试性和可扩展性,是构建企业级应用的必备技能。