第72集:Store定义与模块化
概述
在Pinia中,Store的定义方式和模块化设计是构建复杂应用状态管理的核心。Pinia提供了灵活的Store定义方式,支持Options API和Composition API两种风格,同时支持扁平化的模块化设计,使得状态管理更加清晰和可维护。
核心知识点
1. Store定义方式
Pinia支持两种Store定义方式:Options API风格和Composition API风格。
1.1 Options API风格
Options API风格的Store定义与Vue组件的Options API类似,包含state、getters和actions三个选项:
// stores/user.ts
import { defineStore } from 'pinia'
export interface User {
id: number
name: string
email: string
}
export const useUserStore = defineStore('user', {
// 状态定义
state: () => ({
users: [] as User[],
currentUser: null as User | null,
loading: false,
error: null as string | null
}),
// 计算属性
getters: {
// 基本getter
userCount: (state) => state.users.length,
// getter可以访问其他getters
hasUsers: (state) => state.userCount > 0,
// getter可以返回函数,实现带参数的getter
getUserById: (state) => (id: number) => {
return state.users.find(user => user.id === id)
}
},
// 方法(同步和异步)
actions: {
// 同步action
addUser(user: User) {
this.users.push(user)
},
// 异步action
async fetchUsers() {
this.loading = true
this.error = null
try {
const response = await fetch('https://api.example.com/users')
const data = await response.json()
this.users = data
} catch (err) {
this.error = err instanceof Error ? err.message : 'Failed to fetch users'
} finally {
this.loading = false
}
}
}
})1.2 Composition API风格
Composition API风格的Store定义使用组合式函数,更加灵活和强大:
// stores/product.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface Product {
id: number
name: string
price: number
category: string
}
export const useProductStore = defineStore('product', () => {
// 状态(使用ref/reactive)
const products = ref<Product[]>([])
const selectedCategory = ref<string>('all')
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
// 计算属性(使用computed)
const filteredProducts = computed(() => {
if (selectedCategory.value === 'all') {
return products.value
}
return products.value.filter(product => product.category === selectedCategory.value)
})
const productCount = computed(() => products.value.length)
// 方法
function setSelectedCategory(category: string) {
selectedCategory.value = category
}
async function fetchProducts() {
loading.value = true
error.value = null
try {
const response = await fetch('https://api.example.com/products')
const data = await response.json()
products.value = data
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch products'
} finally {
loading.value = false
}
}
function addProduct(product: Product) {
products.value.push(product)
}
function updateProduct(id: number, updates: Partial<Product>) {
const index = products.value.findIndex(p => p.id === id)
if (index !== -1) {
products.value[index] = { ...products.value[index], ...updates }
}
}
// 返回公开的状态和方法
return {
products,
selectedCategory,
loading,
error,
filteredProducts,
productCount,
setSelectedCategory,
fetchProducts,
addProduct,
updateProduct
}
})2. Store模块化设计
Pinia支持扁平化的模块化设计,每个Store都是独立的模块,无需嵌套。
2.1 按功能模块化
根据应用功能将Store划分为不同的模块:
src/
├── stores/
│ ├── index.ts # 统一出口
│ ├── auth.ts # 认证相关
│ ├── user.ts # 用户管理
│ ├── product.ts # 产品管理
│ ├── cart.ts # 购物车
│ └── order.ts # 订单管理2.2 Store组合
Store之间可以相互组合,实现复杂的业务逻辑:
// stores/cart.ts
import { defineStore } from 'pinia'
import { useProductStore } from './product'
export interface CartItem {
productId: number
quantity: number
}
export const useCartStore = defineStore('cart', () => {
const cartItems = ref<CartItem[]>([])
// 获取其他Store
const productStore = useProductStore()
// 计算属性 - 购物车总价
const totalPrice = computed(() => {
return cartItems.value.reduce((total, item) => {
const product = productStore.products.find(p => p.id === item.productId)
return total + (product ? product.price * item.quantity : 0)
}, 0)
})
// 计算属性 - 购物车商品数量
const totalItems = computed(() => {
return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})
function addToCart(productId: number, quantity: number = 1) {
const existingItem = cartItems.value.find(item => item.productId === productId)
if (existingItem) {
existingItem.quantity += quantity
} else {
cartItems.value.push({ productId, quantity })
}
}
return {
cartItems,
totalPrice,
totalItems,
addToCart
}
})3. Store状态修改
在Pinia中,有多种方式修改Store状态:
3.1 直接修改
在Actions或组件中可以直接修改状态:
// 在action中
actions: {
increment() {
this.count++ // 直接修改
}
}
// 在组件中
const store = useCounterStore()
store.count++ // 直接修改3.2 使用$patch方法
使用$patch方法可以批量修改状态:
// 对象形式
store.$patch({
count: store.count + 1,
name: 'New Name'
})
// 函数形式
store.$patch((state) => {
state.count++
state.name = 'New Name'
})3.3 重置状态
使用$reset方法可以重置Store状态:
store.$reset() // 重置为初始状态4. Store订阅
可以订阅Store的状态变化:
// 订阅状态变化
const unsubscribe = store.$subscribe((mutation, state) => {
console.log('State changed:', mutation, state)
// 可以在这里保存状态到本地存储
localStorage.setItem('storeState', JSON.stringify(state))
})
// 取消订阅
unsubscribe()最佳实践
1. Store设计原则
- 单一职责:每个Store只负责一个功能领域
- 扁平结构:避免嵌套Store,使用扁平化设计
- 清晰命名:使用有意义的名称,如
useUserStore而不是user - 类型安全:为所有状态和方法添加类型定义
2. 目录结构最佳实践
src/
├── stores/
│ ├── index.ts # 统一出口
│ ├── auth/ # 认证模块(复杂功能可以使用子目录)
│ │ ├── index.ts
│ │ ├── state.ts
│ │ ├── getters.ts
│ │ └── actions.ts
│ ├── user.ts # 简单功能直接使用单个文件
│ └── product.ts3. 复杂Store拆分
对于复杂的Store,可以将其拆分为多个文件:
// stores/auth/state.ts
export interface AuthState {
user: User | null
token: string | null
loading: boolean
error: string | null
}
export const useAuthState = () => {
return {
user: ref<User | null>(null),
token: ref<string | null>(localStorage.getItem('token')),
loading: ref(false),
error: ref<string | null>(null)
}
}
// stores/auth/getters.ts
export const useAuthGetters = (state: AuthState) => {
return {
isAuthenticated: computed(() => !!state.token),
isLoading: computed(() => state.loading)
}
}
// stores/auth/actions.ts
export const useAuthActions = (state: AuthState) => {
return {
async login(email: string, password: string) {
// 登录逻辑
},
async logout() {
// 登出逻辑
}
}
}
// stores/auth/index.ts
export const useAuthStore = defineStore('auth', () => {
const state = useAuthState()
const getters = useAuthGetters(state)
const actions = useAuthActions(state)
return {
...state,
...getters,
...actions
}
})4. Store间通信
- 使用
useStore在Store内部获取其他Store - 避免循环依赖
- 对于复杂依赖,考虑使用事件总线或中间件
5. 状态持久化
使用$subscribe方法实现状态持久化:
// stores/index.ts
import { useAuthStore } from './auth'
import { useUserStore } from './user'
// 初始化Store
const authStore = useAuthStore()
const userStore = useUserStore()
// 订阅状态变化,保存到本地存储
authStore.$subscribe((_, state) => {
localStorage.setItem('auth', JSON.stringify({
token: state.token,
user: state.user
}))
})
// 从本地存储恢复状态
const savedAuth = localStorage.getItem('auth')
if (savedAuth) {
const { token, user } = JSON.parse(savedAuth)
authStore.$patch({
token,
user
})
}常见问题与解决方案
1. Store间循环依赖
问题:两个Store相互依赖,导致循环引用错误。
解决方案:
- 重构Store,提取公共逻辑到第三个Store
- 在action内部延迟获取依赖的Store
- 使用事件总线解耦
2. 大型Store难以维护
问题:单个Store包含太多逻辑,难以维护。
解决方案:
- 按功能拆分Store
- 使用Composition API风格,将逻辑拆分为多个组合式函数
- 将复杂Store拆分为多个文件
- 遵循单一职责原则
3. 类型推导失败
问题:TypeScript类型推导失败,导致类型错误。
解决方案:
- 确保tsconfig.json中启用了
strict: true - 为所有状态和方法添加显式类型定义
- 使用类型断言确保类型安全
4. 状态更新不触发组件更新
问题:修改Store状态后,组件没有更新。
解决方案:
- 确保使用
ref或reactive定义状态 - 避免直接修改对象属性,使用
$patch方法 - 确保在组件中正确使用Store状态
进一步学习资源
课后练习
基础练习:
- 创建一个产品管理Store,包含产品列表、分类筛选、搜索功能
- 实现添加、编辑、删除产品的功能
- 在组件中使用该Store
进阶练习:
- 创建一个购物车Store,与产品Store组合
- 实现添加到购物车、修改数量、删除商品功能
- 实现购物车总价计算
- 实现购物车状态持久化
Composition API风格:
- 使用Composition API风格定义一个复杂的Store
- 将其拆分为多个文件(state、getters、actions)
- 在组件中使用该Store
Store组合练习:
- 创建用户Store和订单Store
- 在订单Store中使用用户Store
- 实现订单列表、创建订单、取消订单功能
通过本节课的学习,你应该能够掌握Pinia的Store定义方式和模块化设计,理解Options API和Composition API两种风格的差异,掌握Store组合和模块化设计的最佳实践,能够设计出清晰、可维护的状态管理架构。