第8章 状态管理

第24节 Actions与模块化

8.24.1 Actions异步操作

Actions是Store中用于处理业务逻辑和异步操作的方法。与Getters不同,Actions可以执行异步操作,并且可以修改State。

基本异步Actions

// stores/product.js
import { defineStore } from 'pinia'
import axios from 'axios'

export const useProductStore = defineStore('product', {
  state: () => ({
    products: [],
    loading: false,
    error: null
  }),
  
  actions: {
    // 异步获取产品列表
    async fetchProducts() {
      this.loading = true
      this.error = null
      
      try {
        const response = await axios.get('/api/products')
        this.products = response.data
      } catch (error) {
        this.error = error.message || '获取产品列表失败'
        console.error('Failed to fetch products:', error)
      } finally {
        this.loading = false
      }
    },
    
    // 异步创建产品
    async createProduct(productData) {
      this.loading = true
      this.error = null
      
      try {
        const response = await axios.post('/api/products', productData)
        this.products.push(response.data)
        return response.data // 返回创建的产品
      } catch (error) {
        this.error = error.message || '创建产品失败'
        console.error('Failed to create product:', error)
        throw error // 重新抛出错误,让调用者处理
      } finally {
        this.loading = false
      }
    }
  }
})

在组件中使用异步Actions

<template>
  <div>
    <h1>产品列表</h1>
    
    <!-- 加载状态 -->
    <div v-if="productStore.loading" class="loading">
      加载中...
    </div>
    
    <!-- 错误信息 -->
    <div v-else-if="productStore.error" class="error">
      {{ productStore.error }}
      <button @click="fetchProducts">重试</button>
    </div>
    
    <!-- 产品列表 -->
    <ul v-else>
      <li v-for="product in productStore.products" :key="product.id">
        {{ product.name }} - ¥{{ product.price }}
      </li>
    </ul>
    
    <!-- 创建产品表单 -->
    <form @submit.prevent="createProduct">
      <input v-model="newProduct.name" placeholder="产品名称" required>
      <input v-model.number="newProduct.price" type="number" placeholder="产品价格" required>
      <button type="submit" :disabled="productStore.loading">
        创建产品
      </button>
    </form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useProductStore } from '../stores/product'

const productStore = useProductStore()
const newProduct = ref({ name: '', price: 0 })

// 获取产品列表
const fetchProducts = async () => {
  await productStore.fetchProducts()
}

// 创建产品
const createProduct = async () => {
  try {
    await productStore.createProduct(newProduct.value)
    // 重置表单
    newProduct.value = { name: '', price: 0 }
  } catch (error) {
    // 错误已经在Action中处理,这里可以添加额外的UI反馈
    alert('创建产品失败,请稍后重试')
  }
}

// 组件挂载时获取产品列表
fetchProducts()
</script>

错误处理策略

  1. 在Action内部处理错误

    async fetchProducts() {
      try {
        const response = await axios.get('/api/products')
        this.products = response.data
      } catch (error) {
        this.error = error.message
        // 可以在这里添加错误日志、上报等
      }
    }
  2. 重新抛出错误

    async createProduct(productData) {
      try {
        const response = await axios.post('/api/products', productData)
        this.products.push(response.data)
        return response.data
      } catch (error) {
        this.error = error.message
        throw error // 让调用者也能处理错误
      }
    }
  3. 使用Promise.all处理多个异步操作

    async fetchMultipleData() {
      try {
        const [products, categories] = await Promise.all([
          axios.get('/api/products'),
          axios.get('/api/categories')
        ])
        this.products = products.data
        this.categories = categories.data
      } catch (error) {
        this.error = '获取数据失败'
      }
    }
  4. 取消请求

    async fetchProducts() {
      // 创建取消令牌
      const source = axios.CancelToken.source()
      this.cancelToken = source
      
      try {
        const response = await axios.get('/api/products', {
          cancelToken: source.token
        })
        this.products = response.data
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log('请求被取消:', error.message)
        } else {
          this.error = error.message
        }
      }
    },
    
    // 取消请求的方法
    cancelFetch() {
      if (this.cancelToken) {
        this.cancelToken.cancel('用户取消了请求')
      }
    }

8.24.2 模块化状态管理

当应用规模增长时,我们需要将Store拆分为多个模块,每个模块负责一个特定的业务领域。Pinia天然支持模块化,每个Store就是一个独立的模块。

模块化Store结构

src/
├── stores/
│   ├── index.js          # Store出口文件
│   ├── auth.js           # 认证相关Store
│   ├── user.js           # 用户相关Store
│   ├── product.js        # 产品相关Store
│   ├── cart.js           # 购物车相关Store
│   └── order.js          # 订单相关Store
└── ...

创建模块化Store

// stores/auth.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: localStorage.getItem('token') || null,
    user: null,
    loading: false,
    error: null
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token,
    currentUser: (state) => state.user
  },
  
  actions: {
    async login(credentials) {
      // 登录逻辑
    },
    
    async logout() {
      // 登出逻辑
    },
    
    async getProfile() {
      // 获取用户信息逻辑
    }
  }
})
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    loading: false,
    error: null
  }),
  
  actions: {
    async fetchUsers() {
      // 获取用户列表逻辑
    },
    
    async updateUser(userId, userData) {
      // 更新用户逻辑
    }
  }
})

统一出口文件

创建一个index.js文件作为所有Store的统一出口,方便组件导入:

// stores/index.js
// 导出所有Store
export { useAuthStore } from './auth'
export { useUserStore } from './user'
export { useProductStore } from './product'
export { useCartStore } from './cart'
export { useOrderStore } from './order'

在组件中导入使用:

// 从统一出口导入
import { useAuthStore, useProductStore } from '../stores'

const authStore = useAuthStore()
const productStore = useProductStore()

8.24.3 Store间相互调用

在复杂应用中,我们经常需要在一个Store中调用另一个Store的方法或访问其状态。Pinia提供了简单的方式来实现Store间的通信。

在Action中调用其他Store

// stores/cart.js
import { defineStore } from 'pinia'
import { useProductStore } from './product' // 导入其他Store

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    total: 0
  }),
  
  getters: {
    cartCount: (state) => state.items.length,
    isEmpty: (state) => state.items.length === 0
  },
  
  actions: {
    // 添加商品到购物车
    addToCart(productId, quantity = 1) {
      const productStore = useProductStore() // 获取其他Store实例
      const product = productStore.products.find(p => p.id === productId)
      
      if (!product) {
        console.error('Product not found:', productId)
        return
      }
      
      // 检查购物车中是否已存在该商品
      const existingItem = this.items.find(item => item.id === productId)
      
      if (existingItem) {
        // 已存在,增加数量
        existingItem.quantity += quantity
      } else {
        // 不存在,添加新商品
        this.items.push({
          ...product,
          quantity
        })
      }
      
      // 更新总价
      this.updateTotal()
    },
    
    // 更新购物车总价
    updateTotal() {
      this.total = this.items.reduce((sum, item) => {
        return sum + (item.price * item.quantity)
      }, 0)
    },
    
    // 清空购物车
    clearCart() {
      this.items = []
      this.total = 0
    }
  }
})

在Getters中访问其他Store

// stores/order.js
import { defineStore } from 'pinia'
import { useCartStore } from './cart'

export const useOrderStore = defineStore('order', {
  state: () => ({
    orders: [],
    currentOrder: null
  }),
  
  getters: {
    // 访问其他Store的状态
    cartItemsCount: () => {
      const cartStore = useCartStore()
      return cartStore.cartCount
    },
    
    // 访问其他Store的Getters
    isCartEmpty: () => {
      const cartStore = useCartStore()
      return cartStore.isEmpty
    },
    
    // 结合多个Store的数据
    orderSummary: (state) => {
      const cartStore = useCartStore()
      return {
        items: cartStore.items,
        total: cartStore.total,
        orderCount: state.orders.length
      }
    }
  },
  
  actions: {
    // 创建订单
    async createOrder() {
      const cartStore = useCartStore()
      
      if (cartStore.isEmpty) {
        throw new Error('购物车为空,无法创建订单')
      }
      
      // 创建订单逻辑
      const order = {
        items: cartStore.items,
        total: cartStore.total,
        createdAt: new Date().toISOString()
      }
      
      // 保存订单
      this.orders.push(order)
      this.currentOrder = order
      
      // 清空购物车
      cartStore.clearCart()
      
      return order
    }
  }
})

8.24.4 状态持久化方案

在实际应用中,我们经常需要将某些状态持久化到本地存储(如localStorage、sessionStorage)中,以保持页面刷新后状态不丢失。

手动持久化

// stores/auth.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    // 从localStorage读取初始状态
    token: localStorage.getItem('token') || null,
    user: JSON.parse(localStorage.getItem('user')) || null
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token
  },
  
  actions: {
    async login(credentials) {
      // 登录逻辑,获取token和user
      const response = await api.login(credentials)
      
      // 更新状态
      this.token = response.token
      this.user = response.user
      
      // 保存到localStorage
      localStorage.setItem('token', this.token)
      localStorage.setItem('user', JSON.stringify(this.user))
    },
    
    async logout() {
      // 登出逻辑
      await api.logout(this.token)
      
      // 清空状态
      this.token = null
      this.user = null
      
      // 从localStorage移除
      localStorage.removeItem('token')
      localStorage.removeItem('user')
    },
    
    // 监听状态变化,自动保存到localStorage
    $persist() {
      localStorage.setItem('token', this.token)
      localStorage.setItem('user', JSON.stringify(this.user))
    }
  }
})

// 在main.js中添加全局监听
pinia.use(({ store }) => {
  // 监听状态变化
  store.$subscribe(() => {
    // 调用每个Store的$persist方法(如果存在)
    if (typeof store.$persist === 'function') {
      store.$persist()
    }
  })
})

使用pinia-plugin-persistedstate插件

Pinia提供了官方推荐的持久化插件pinia-plugin-persistedstate,使用它可以更方便地实现状态持久化。

  1. 安装插件

    npm install pinia-plugin-persistedstate
  2. 配置插件

    // main.js
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    import App from './App.vue'
    
    const pinia = createPinia()
    pinia.use(piniaPluginPersistedstate) // 使用持久化插件
    
    const app = createApp(App)
    app.use(pinia)
    app.mount('#app')
  3. 在Store中启用持久化

    // stores/auth.js
    import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
state: () => ({
token: null,
user: null
}),

getters: {
isAuthenticated: (state) => !!state.token
},

actions: {
async login(credentials) {
// 登录逻辑
},

async logout() {
  // 登出逻辑
}

},

// 启用持久化
persist: true
})


4. **自定义持久化配置**:
```javascript
// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
 items: [],
 total: 0
  }),
  
  // 自定义持久化配置
  persist: {
 // 存储键名
 key: 'my-app-cart',
 
 // 存储方式:localStorage、sessionStorage或自定义存储
 storage: sessionStorage,
 
 // 指定需要持久化的状态字段
 paths: ['items'], // 只持久化items,不持久化total
 
 // 序列化和反序列化方法
 serializer: {
   serialize: (value) => JSON.stringify(value),
   deserialize: (value) => JSON.parse(value)
 },
 
 // 持久化钩子
 beforeRestore: (context) => {
   console.log('Before restoring cart state:', context)
 },
 afterRestore: (context) => {
   console.log('After restoring cart state:', context)
   // 可以在这里重新计算total
   context.store.updateTotal()
 }
  },
  
  actions: {
 updateTotal() {
   this.total = this.items.reduce((sum, item) => {
     return sum + (item.price * item.quantity)
   }, 0)
 }
  }
})

最佳实践与注意事项

  1. Action命名规范

    • 使用动词开头,如fetchProductscreateUserupdateProfile
    • 清晰表达Action的意图和功能
    • 避免使用模糊的名称,如doSomething
  2. 异步Action最佳实践

    • 始终处理错误,避免未捕获的Promise错误
    • 使用loading状态反馈给用户
    • 提供取消异步操作的机制
    • 考虑添加重试机制
  3. 模块化设计原则

    • 每个Store负责一个特定的业务领域
    • Store之间的依赖关系要清晰
    • 避免循环依赖
    • 使用统一的出口文件管理所有Store
  4. 状态持久化注意事项

    • 不要持久化敏感数据(如密码、令牌的有效期)
    • 考虑数据大小限制(localStorage通常限制为5MB)
    • 对于大型数据,考虑使用IndexedDB
    • 定期清理过期数据
  5. 性能优化

    • 避免在Action中执行过多同步操作
    • 对于频繁更新的状态,考虑使用防抖或节流
    • 合理使用缓存,避免重复请求
    • 考虑使用虚拟滚动处理大型列表

小结

本节我们学习了Pinia中的Actions与模块化,包括:

  • 异步Actions的编写和使用
  • 错误处理策略
  • 模块化Store的设计和组织
  • Store间的相互调用
  • 状态持久化方案

通过合理使用Actions和模块化设计,我们可以构建清晰、可维护的状态管理系统。状态持久化则可以提高用户体验,保持页面刷新后状态不丢失。

思考与练习

  1. 创建一个包含异步操作的Store,实现数据的CRUD功能。
  2. 设计一个模块化的Store结构,包含认证、用户、产品等模块。
  3. 实现Store间的相互调用,如购物车Store调用产品Store的数据。
  4. 为认证Store添加状态持久化,使用localStorage保存令牌和用户信息。
  5. 尝试使用pinia-plugin-persistedstate插件实现状态持久化。
  6. 设计一个错误处理机制,包含加载状态、错误信息和重试功能。
« 上一篇 22-state-getters 下一篇 » 24-typescript-basics