Skip to content
On this page

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应用的关键架构决策。通过本章的学习,你应该掌握:

  1. 基础模块划分:如何合理地拆分应用状态
  2. Store间通信:不同Store之间的数据交互方式
  3. Namespaced Store:按功能或领域组织Store
  4. 依赖管理:管理Store之间的依赖关系
  5. 动态注册:按需加载和卸载Store
  6. 测试策略:模块化Store的测试方法
  7. 架构最佳实践:分层架构和关系管理

模块化的Store架构提高了代码的可维护性、可测试性和可扩展性,是构建企业级应用的必备技能。