状态管理(Pinia)

yarn add pinia@latest

plaintext
复制代码

安装完成后,需在项目入口文件中创建 Pinia 实例并挂载到 Vue 应用。在典型的 Vue 3.5 + TypeScript 项目中,推荐在 `src/store/index.ts` 中集中管理 Store 实例:

```typescript
// src/store/index.ts
import { createPinia } from 'pinia'

// 创建 Pinia 实例
const pinia = createPinia()

export default pinia

随后在 main.ts 中通过 app.use() 方法挂载:

typescript
复制代码
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import pinia from './store' // 导入 Pinia 实例

const app = createApp(App)
app.use(pinia) // 挂载 Pinia
app.mount('#app')

与 Vuex 的模块化差异是 Pinia 的核心优势之一。传统 Vuex 通过 modules 选项实现模块化,存在嵌套层级深、命名空间冲突等问题。而 Pinia 采用扁平化 Store 设计,无需嵌套 modules,直接通过定义多个独立 Store 文件实现模块化拆分。每个 Store 都是一个独立的模块,通过 defineStore 函数创建,天然支持命名空间隔离。例如购物车模块直接定义为 cartStore,用户模块定义为 userStore,模块间通过导入函数独立调用,避免了 Vuex 中 this.$store.dispatch('cart/addItem') 式的路径拼接,大幅提升代码可读性和维护性。

初始化注意事项

  • Pinia 需在应用挂载前完成初始化,确保所有组件能访问到 Store 实例
  • TypeScript 环境下无需额外配置类型声明,Pinia 原生支持类型推导
  • 若使用 Vue 2.x,需安装 @vue/composition-api 兼容依赖,但 Vue 3.5 可直接使用

Store 定义与核心结构(1500字)

Store 是 Pinia 的核心概念,代表一个独立的状态管理单元。以"购物车"场景为例,我们通过 defineStore 函数定义 cartStore,包含 state(状态)getters(计算属性)actions(方法) 三要素,并全程使用 TypeScript 类型约束确保类型安全。

基础结构与 TypeScript 类型定义

首先定义购物车商品的接口类型,明确数据结构:

typescript
复制代码
// src/store/cartStore.ts
import { defineStore } from 'pinia'

// 定义购物车商品类型接口
interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
  imageUrl?: string // 可选的商品图片URL
}

// 定义 Store 类型,用于类型推导(可选但推荐)
interface CartState {
  items: CartItem[]
  isLoading: boolean // 加载状态标识
}

接着通过 defineStore 创建 Store,第一个参数为唯一 ID(用于 DevTools 调试和模块隔离),第二个参数为配置对象:

typescript
复制代码
// 定义 cartStore
export const useCartStore = defineStore('cart', {
  // 状态定义:返回初始状态的函数(避免服务端渲染时的状态污染)
  state: (): CartState => ({
    items: [], // 购物车商品数组
    isLoading: false // 初始加载状态为 false
  }),

  // Getters:计算属性,基于 state 派生状态
  getters: {
    // 计算商品总数:累加所有商品的 quantity
    totalQuantity: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
    
    // 计算商品总价:单价 × 数量 累加
    totalPrice: (state) => state.items.reduce(
      (sum, item) => sum + (item.price * item.quantity), 
      0
    )
  },

  // Actions:修改状态的方法(支持同步和异步)
  actions: {
    // 同步 Action:添加商品到购物车
    addItem(item: CartItem) {
      // 检查商品是否已存在,若存在则更新数量
      const existingItem = this.items.find(i => i.id === item.id)
      if (existingItem) {
        existingItem.quantity += item.quantity
      } else {
        this.items.push(item) // 新商品直接添加
      }
    },

    // 异步 Action:从 API 加载购物车数据(返回 Promise)
    async fetchItems() {
      this.isLoading = true // 开始加载,更新状态
      try {
        // 模拟 API 请求(实际项目替换为真实接口)
        const response = await fetch('/api/cart')
        const data: CartItem[] = await response.json()
        this.items = data // 成功后更新状态
        return data // 返回加载结果
      } catch (error) {
        console.error('Failed to fetch cart items:', error)
        throw error // 抛出错误供组件捕获
      } finally {
        this.isLoading = false // 无论成功失败,结束加载状态
      }
    }
  }
})
Store 的使用方式

在组件中使用 Store 需通过定义时导出的 useCartStore 函数(遵循 Vue 组合式 API 命名规范):

plaintext
复制代码
<!-- src/components/Cart.vue -->
<template>
  <div class="cart">
    <h2>购物车 ({{ cartStore.totalQuantity }})</h2>
    <div v-if="cartStore.isLoading">加载中...</div>
    <div v-else>
      <p>总价: ¥{{ cartStore.totalPrice.toFixed(2) }}</p>
      <button @click="loadCart">加载购物车</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useCartStore } from '@/store/cartStore'

// 创建 Store 实例(注意:每次调用返回同一个实例,无需担心重复创建)
const cartStore = useCartStore()

// 调用异步 Action 加载数据
const loadCart = async () => {
  try {
    await cartStore.fetchItems()
    alert('购物车加载成功')
  } catch (error) {
    alert('加载失败,请重试')
  }
}
</script>

TypeScript 类型优势在上述代码中得到充分体现:addItem 方法强制要求传入符合 CartItem 接口的参数,避免类型错误;state 中的 items 数组被严格约束为 CartItem[] 类型,杜绝非法数据写入;组件中调用 cartStore.totalPrice 时,TypeScript 能自动推导其为 number 类型,提供完整的代码提示和编译时校验。

Getters 与 Actions 深入解析(1500字)

Pinia 的 Getters 和 Actions 不仅实现了状态的派生与修改,还内置了性能优化和响应式处理机制。深入理解其工作原理,能帮助开发者写出更高效、更健壮的状态管理逻辑。

Getters:带缓存的计算属性

Pinia 的 Getters 本质是依赖追踪的计算属性,与 Vue 的 computed 具有相同的缓存机制:当依赖的 state 字段未发生变化时,多次调用 Getters 会直接返回缓存结果,避免重复计算。以 totalPrice 为例:

typescript
复制代码
// 组件中调用 Getters
console.log(cartStore.totalPrice) // 首次调用:计算并缓存结果(假设 100)
console.log(cartStore.totalPrice) // 第二次调用:直接返回缓存的 100,不重新计算
cartStore.addItem({ id: '1', name: '商品', price: 50, quantity: 1 }) // 修改 state
console.log(cartStore.totalPrice) // 依赖变化:重新计算并缓存新结果(150)

缓存机制验证可通过在 Getters 中添加日志实现:

typescript
复制代码
getters: {
  totalPrice: (state) => {
    console.log('Calculating total price...')
    return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  }
}

此时连续调用 totalPrice 仅在首次和 state 变化后打印日志,证明缓存生效。这一特性在复杂计算场景(如大数据列表过滤、统计)中能显著提升性能。

Actions:状态修改与错误处理

Actions 是修改 state 的唯一推荐方式(不建议直接修改 state,如 cartStore.items.push(...)),支持同步和异步操作,且天然绑定 Store 实例(无需 this 绑定)。

异步 Action 的错误处理是生产环境必备能力。在 fetchItems 中,通过 try/catch 包裹 API 请求,可优雅处理网络错误、数据格式错误等异常:

typescript
复制代码
async fetchItems() {
  this.isLoading = true
  try {
    const response = await fetch('/api/cart')
    if (!response.ok) throw new Error(`HTTP error: ${response.status}`) // 处理 HTTP 错误状态码
    const data: CartItem[] = await response.json()
    // 数据验证:确保返回数组且每个元素符合 CartItem 结构
    if (!Array.isArray(data)) throw new Error('Invalid cart data format')
    this.items = data
    return { success: true, data }
  } catch (error) {
    // 错误分类处理
    if (error instanceof TypeError) {
      console.error('Network error:', error)
    } else {
      console.error('Data error:', error)
    }
    this.items = [] // 错误时清空无效数据
    return { success: false, error: error.message }
  } finally {
    this.isLoading = false
  }
}

响应式状态的解构需使用 storeToRefs 工具函数。直接解构 Store 的 state 会丢失响应式(如 const { items } = cartStore),而 storeToRefs 能将 state 字段转换为 ref 对象,保持响应式:

typescript
复制代码
import { storeToRefs } from 'pinia'

const cartStore = useCartStore()
// 解构响应式状态(ref 对象)
const { items, isLoading } = storeToRefs(cartStore)
// 非响应式属性(如 getters、actions)直接解构
const { totalPrice, addItem } = cartStore

此时模板中可直接使用 items.value(或在模板中省略 .value),且当 items 变化时,组件会自动重新渲染。

最佳实践

  • Getters 仅用于派生状态,不修改 state;复杂逻辑可拆分为多个 Getters 组合使用
  • Actions 命名采用动词开头(如 addItemfetchItems),明确功能意图
  • 异步 Action 必须返回 Promise,便于组件处理加载状态和结果
  • 避免在 Action 中直接操作 DOM 或路由,保持状态管理与 UI 逻辑分离

模块化与持久化(1000字)

随着应用规模增长,单一 Store 会变得臃肿。Pinia 的扁平化模块化设计允许按业务域拆分多个独立 Store,同时通过插件实现状态持久化,解决页面刷新后状态丢失问题。

多 Store 模块化拆分

以电商应用为例,可拆分为 cartStore(购物车)、userStore(用户信息)、productStore(商品列表)等独立模块。每个模块单独文件管理:

plaintext
复制代码
src/store/
├── cartStore.ts    // 购物车状态
├── userStore.ts    // 用户状态
└── index.ts        // 导出 Pinia 实例

用户模块示例userStore.ts):

typescript
复制代码
// src/store/userStore.ts
import { defineStore } from 'pinia'

interface User {
  id: string
  name: string
  avatar?: string
}

interface UserState {
  user: User | null
  token: string | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    user: null,
    token: localStorage.getItem('token') || null // 初始化时从本地存储读取
  }),
  actions: {
    login(token: string, userData: User) {
      this.token = token
      this.user = userData
      localStorage.setItem('token', token) // 临时存储,实际项目建议用持久化插件
    },
    logout() {
      this.token = null
      this.user = null
      localStorage.removeItem('token')
    }
  }
})

在组件中同时使用多个 Store 时,通过导入对应 useXxxStore 函数即可,模块间天然隔离:

plaintext
复制代码
<script setup lang="ts">
import { useCartStore } from '@/store/cartStore'
import { useUserStore } from '@/store/userStore'

const cartStore = useCartStore()
const userStore = useUserStore()

// 组合使用两个 Store 的状态和方法
const checkout = () => {
  if (!userStore.token) {
    alert('请先登录')
    return
  }
  console.log('Checkout with items:', cartStore.items)
}
</script>
状态持久化实现

默认情况下,Pinia 状态存储在内存中,页面刷新后会丢失。通过 pinia-plugin-persistedstate 插件可将状态持久化到 localStoragesessionStorage

安装与配置步骤

  1. 安装插件:
bash
复制代码
npm install pinia-plugin-persistedstate
  1. 在 Pinia 初始化时注册插件:
typescript
复制代码
// src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 注册持久化插件

export default pinia
  1. 在 Store 中启用持久化:
typescript
复制代码
// src/store/cartStore.ts
export const useCartStore = defineStore('cart', {
  state: () => ({ /* ... */ }),
  getters: { /* ... */ },
  actions: { /* ... */ },
  // 持久化配置
  persist: {
    enabled: true, // 启用持久化
    strategies: [
      {
        key: 'cart-storage', // 存储键名,默认是 Store 的 id
        storage: localStorage, // 存储位置:localStorage/sessionStorage
        paths: ['items'] // 指定需要持久化的 state 字段(默认所有字段)
      }
    ]
  }
})

关键配置项解析

  • paths:数组类型,指定需要持久化的字段。例如 paths: ['items'] 仅持久化 items,忽略 isLoading,减少存储冗余
  • storage:选择存储介质,localStorage(永久存储)或 sessionStorage(会话级存储)
  • beforeRestore/afterRestore:钩子函数,可在恢复前/后处理数据(如解密/加密)

配置后,购物车的 items 数组会在修改时自动保存到 localStorage,页面刷新后自动恢复,实现"刷新不丢失"的用户体验。

持久化注意事项

  • 仅能持久化可序列化的数据(如对象、数组、基本类型),函数、Symbol 等无法持久化
  • 敏感数据(如用户 token)建议加密存储,避免直接暴露在 localStorage 中
  • 频繁修改的状态(如倒计时)不建议持久化,可能导致性能问题

通过本章学习,我们掌握了 Pinia 从安装到实战的完整流程:通过扁平化 Store 设计简化模块化管理,利用 TypeScript 类型系统确保代码健壮性,借助 Getters 缓存和 Actions 异步处理优化性能,最终通过持久化插件实现状态的跨会话保留。相比传统状态管理方案,Pinia 以更简洁的 API、更优的性能和更完善的类型支持,成为 Vue.js 3.5 应用的首选状态管理方案[24]。在实际开发中,建议结合业务场景合理拆分 Store,遵循"单一职责"原则,让状态管理逻辑清晰可维护。