第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>错误处理策略
在Action内部处理错误:
async fetchProducts() { try { const response = await axios.get('/api/products') this.products = response.data } catch (error) { this.error = error.message // 可以在这里添加错误日志、上报等 } }重新抛出错误:
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 // 让调用者也能处理错误 } }使用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 = '获取数据失败' } }取消请求:
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,使用它可以更方便地实现状态持久化。
安装插件:
npm install pinia-plugin-persistedstate配置插件:
// 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')在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)
}
}
})最佳实践与注意事项
Action命名规范:
- 使用动词开头,如
fetchProducts、createUser、updateProfile - 清晰表达Action的意图和功能
- 避免使用模糊的名称,如
doSomething
- 使用动词开头,如
异步Action最佳实践:
- 始终处理错误,避免未捕获的Promise错误
- 使用
loading状态反馈给用户 - 提供取消异步操作的机制
- 考虑添加重试机制
模块化设计原则:
- 每个Store负责一个特定的业务领域
- Store之间的依赖关系要清晰
- 避免循环依赖
- 使用统一的出口文件管理所有Store
状态持久化注意事项:
- 不要持久化敏感数据(如密码、令牌的有效期)
- 考虑数据大小限制(localStorage通常限制为5MB)
- 对于大型数据,考虑使用IndexedDB
- 定期清理过期数据
性能优化:
- 避免在Action中执行过多同步操作
- 对于频繁更新的状态,考虑使用防抖或节流
- 合理使用缓存,避免重复请求
- 考虑使用虚拟滚动处理大型列表
小结
本节我们学习了Pinia中的Actions与模块化,包括:
- 异步Actions的编写和使用
- 错误处理策略
- 模块化Store的设计和组织
- Store间的相互调用
- 状态持久化方案
通过合理使用Actions和模块化设计,我们可以构建清晰、可维护的状态管理系统。状态持久化则可以提高用户体验,保持页面刷新后状态不丢失。
思考与练习
- 创建一个包含异步操作的Store,实现数据的CRUD功能。
- 设计一个模块化的Store结构,包含认证、用户、产品等模块。
- 实现Store间的相互调用,如购物车Store调用产品Store的数据。
- 为认证Store添加状态持久化,使用localStorage保存令牌和用户信息。
- 尝试使用
pinia-plugin-persistedstate插件实现状态持久化。 - 设计一个错误处理机制,包含加载状态、错误信息和重试功能。