第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.ts

3. 复杂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状态后,组件没有更新。

解决方案

  • 确保使用refreactive定义状态
  • 避免直接修改对象属性,使用$patch方法
  • 确保在组件中正确使用Store状态

进一步学习资源

  1. Pinia官方文档 - Store定义
  2. Pinia官方文档 - 模块化
  3. Vue 3 Composition API
  4. TypeScript官方文档
  5. Vue 3 + Pinia最佳实践

课后练习

  1. 基础练习

    • 创建一个产品管理Store,包含产品列表、分类筛选、搜索功能
    • 实现添加、编辑、删除产品的功能
    • 在组件中使用该Store
  2. 进阶练习

    • 创建一个购物车Store,与产品Store组合
    • 实现添加到购物车、修改数量、删除商品功能
    • 实现购物车总价计算
    • 实现购物车状态持久化
  3. Composition API风格

    • 使用Composition API风格定义一个复杂的Store
    • 将其拆分为多个文件(state、getters、actions)
    • 在组件中使用该Store
  4. Store组合练习

    • 创建用户Store和订单Store
    • 在订单Store中使用用户Store
    • 实现订单列表、创建订单、取消订单功能

通过本节课的学习,你应该能够掌握Pinia的Store定义方式和模块化设计,理解Options API和Composition API两种风格的差异,掌握Store组合和模块化设计的最佳实践,能够设计出清晰、可维护的状态管理架构。

« 上一篇 Pinia设计理念与安装 - Vue 3官方状态管理库 下一篇 » State状态管理与响应式 - Pinia核心原理