第十章:Pinia 高级用法(从工程化到性能优化)

一、入门:Pinia 高级特性的核心价值

Pinia 作为 Vue 官方状态管理库,其高级特性旨在解决复杂项目的状态治理问题——包括跨 Store 通信、状态持久化、调试与类型安全增强、性能优化等。这些特性能让状态管理从“简单数据存储”升级为“可维护、可扩展、高性能”的工程化方案,是中大型 Vue3 项目的必备技能。

二、精通:Pinia 高级特性全场景实战

1. 跨 Store 通信(解决多模块状态依赖)

在中大型项目中,Store 会按业务模块拆分(如 userStorecartStoreorderStore),不可避免出现“Store 间数据依赖”。Pinia 提供两种规范的跨 Store 通信方案,避免硬编码耦合。

(1)方案1:Store 内直接引入依赖 Store(推荐轻量场景)

当 Store A 仅需“读取”Store B 的数据或“调用”Store B 的同步 Action 时,可在 Store A 内直接创建 Store B 实例,无需通过组件中转,代码更简洁。

示例:cartStore 依赖 userStore 的用户 ID 关联购物车

// src/store/userStore.js(用户 Store)
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    userId: '', // 用户唯一标识
    userLevel: 0 // 用户等级(影响购物车折扣)
  }),
  actions: {
    // 同步 Action:更新用户等级
    updateUserLevel(level) {
      this.userLevel = level
    }
  }
})

// src/store/cartStore.js(购物车 Store,依赖 userStore)
import { defineStore } from 'pinia'
import { useUserStore } from './userStore' // 直接引入依赖 Store

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] // 购物车商品:[{ goodsId, count, price, discount }]
  }),
  getters: {
    // 计算属性:根据用户等级计算折扣后总价(依赖 userStore 的 userLevel)
    discountedTotalPrice(state) {
      const userStore = useUserStore() // 在 getter 内创建依赖 Store 实例
      // 不同用户等级对应不同折扣(0级:无折扣,1级:9.5折,2级:9折)
      const discount = [1, 0.95, 0.9][userStore.userLevel] || 1
      return state.items.reduce((total, item) => total + item.price * item.count * discount, 0)
    },
    // 筛选当前用户的购物车(避免多用户数据混淆)
    userCartItems(state) {
      const userStore = useUserStore()
      return state.items.filter(item => item.userId === userStore.userId)
    }
  },
  actions: {
    // 异步 Action:获取当前用户的购物车数据(依赖 userStore 的 userId)
    async fetchUserCart() {
      const userStore = useUserStore()
      if (!userStore.userId) throw new Error('用户未登录,无法获取购物车')
      
      // 调用接口时携带用户 ID
      const res = await api.get(`/cart?userId=${userStore.userId}`)
      this.items = res.data.items
    }
  }
})

注意事项

  • 禁止在 Store A 的 state 中直接存储 Store B 的数据(如 state: () => ({ userLevel: useUserStore().userLevel })),会导致状态同步失效;
  • 若需“修改”Store B 的数据,必须调用 Store B 的 Action(而非直接修改 userStore.userLevel),遵循“单一数据源修改”原则。
(2)方案2:通过 pinia 实例实现 Store 注册与通信(复杂场景)

当项目存在“多 Store 循环依赖”(如 A 依赖 B,B 依赖 A)或需“全局管理 Store 实例”时,可通过 Pinia 全局实例(createPinia() 返回的实例)注册 Store,再通过实例获取依赖,避免循环引用报错。

步骤1:创建全局 Pinia 实例并注册 Store

// src/store/index.js(Store 入口文件)
import { createPinia } from 'pinia'
import { useUserStore } from './userStore'
import { useCartStore } from './cartStore'

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

// 注册 Store 到 Pinia 实例(绑定实例,避免循环依赖)
export const useUserStoreWithPinia = () => useUserStore(pinia)
export const useCartStoreWithPinia = () => useCartStore(pinia)

步骤2:在循环依赖的 Store 中使用注册的实例

// src/store/orderStore.js(订单 Store,依赖 userStore 和 cartStore)
import { defineStore } from 'pinia'
import { useUserStoreWithPinia, useCartStoreWithPinia } from './index'

export const useOrderStore = defineStore('order', {
  actions: {
    // 异步 Action:从购物车创建订单(依赖用户 ID 和购物车选中商品)
    async createOrderFromCart() {
      const userStore = useUserStoreWithPinia()
      const cartStore = useCartStoreWithPinia()
      
      if (!userStore.userId) throw new Error('用户未登录')
      // 获取购物车选中商品(依赖 cartStore 的 getter)
      const selectedItems = cartStore.userCartItems.filter(item => item.checked)
      if (selectedItems.length === 0) throw new Error('请选择商品')
      
      // 调用创建订单接口
      const res = await api.post('/order', {
        userId: userStore.userId,
        items: selectedItems.map(item => ({ goodsId: item.goodsId, count: item.count }))
      })
      
      // 创建订单后清空购物车选中商品(调用 cartStore 的 Action)
      await cartStore.clearSelectedItems()
      return res.data.orderId
    }
  }
})

2. 状态持久化(解决页面刷新状态丢失)

Pinia 状态默认存储在内存中,页面刷新后会丢失(如用户登录状态、购物车数据)。需通过“状态持久化”将关键状态存储到 localStorage/sessionStorage,刷新后自动恢复。推荐两种实现方案:自定义持久化逻辑(灵活)、pinia-plugin-persistedstate 插件(便捷)。

(1)方案1:自定义持久化逻辑(精准控制)

适用于需“部分状态持久化”“加密存储”或“自定义存储时机”的场景,通过 Pinia 的 onMounted(Store 初始化)和 watch(状态变化)实现。

示例:userStore 持久化用户 ID 和 Token(加密存储)

// src/utils/encryption.js(简易加密工具,实际项目推荐用 CryptoJS)
export const encrypt = (data) => {
  // 简单 Base64 加密(生产环境需用 AES 等强加密)
  return btoa(JSON.stringify(data))
}

export const decrypt = (str) => {
  try {
    return JSON.parse(atob(str))
  } catch (e) {
    return null // 解密失败返回 null
  }
}

// src/store/userStore.js(自定义持久化)
import { defineStore } from 'pinia'
import { encrypt, decrypt } from '@/utils/encryption'

export const useUserStore = defineStore('user', {
  state: () => ({
    userId: '',
    token: '',
    userInfo: {} // 非持久化状态(如用户昵称、头像,可重新请求)
  }),
  actions: {
    // 登录成功后更新状态并持久化
    loginSuccess(data) {
      this.userId = data.userId
      this.token = data.token
      this.userInfo = data.userInfo
      this.persistState() // 主动触发持久化
    },
    // 退出登录后清空状态并删除持久化数据
    logout() {
      this.userId = ''
      this.token = ''
      this.userInfo = {}
      localStorage.removeItem('pinia_user') // 删除存储
    },
    // 持久化方法:将关键状态加密存储到 localStorage
    persistState() {
      const persistData = { userId: this.userId, token: this.token }
      localStorage.setItem('pinia_user', encrypt(persistData))
    },
    // 恢复持久化状态:Store 初始化时调用
    restoreState() {
      const persistStr = localStorage.getItem('pinia_user')
      if (!persistStr) return
      
      const persistData = decrypt(persistStr)
      if (persistData) {
        this.userId = persistData.userId
        this.token = persistData.token
      }
    }
  }
})

// 在 main.js 中初始化 Store 并恢复状态
import { createApp } from 'vue'
import { pinia } from '@/store'
import { useUserStore } from '@/store/userStore'
import App from './App.vue'

const app = createApp(App)
app.use(pinia)

// 初始化 userStore 并恢复持久化状态
const userStore = useUserStore()
userStore.restoreState()

app.mount('#app')
(2)方案2:使用 pinia-plugin-persistedstate 插件(便捷)

适用于“全状态/多 Store 批量持久化”场景,插件自动监听状态变化并存储,无需手动写 watchlocalStorage 逻辑。

步骤1:安装插件

npm install pinia-plugin-persistedstate -S

步骤2:全局注册插件

// src/store/index.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

// 创建 Pinia 实例并注册持久化插件
export const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

步骤3:配置 Store 持久化规则
通过 Store 配置的 persist 属性指定持久化方式(存储位置、字段、加密等):

// src/store/cartStore.js(购物车 Store 持久化)
import { defineStore } from 'pinia'
import { encrypt, decrypt } from '@/utils/encryption'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [], // 购物车商品(需持久化)
    selectedIds: [] // 选中商品 ID(需持久化)
  }),
  // 持久化配置
  persist: {
    key: 'pinia_cart', // 存储的 key(默认是 Store 名,如 'cart')
    storage: sessionStorage, // 存储位置(默认 localStorage,可选 sessionStorage)
    paths: ['items', 'selectedIds'], // 需持久化的字段(默认全量,推荐显式指定)
    // 自定义序列化(加密)
    serializer: {
      serialize: (data) => encrypt(data), // 存储前加密
      deserialize: (str) => decrypt(str) // 读取前解密
    },
    // 自定义存储时机(默认状态变化即存储,可延迟存储减少性能消耗)
    delay: 300 // 状态变化后延迟 300ms 存储(避免频繁修改导致的 localStorage 写入)
  }
})

3. Getter 高级特性(缓存、参数化、组合)

Pinia 的 Getter 并非简单的“计算属性”,而是具备缓存优化参数化查询组合复用能力,能高效处理复杂状态衍生逻辑。

(1)Getter 缓存机制与失效条件

Pinia Getter 会自动缓存计算结果,只有当“依赖的状态(state/getter)变化”时才重新计算,避免重复执行复杂逻辑(如数组过滤、排序)。

示例:复杂 Getter 的缓存验证

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [
      { goodsId: 1, name: '手机', price: 5999, count: 1, category: 'electronics' },
      { goodsId: 2, name: '衬衫', price: 299, count: 2, category: 'clothes' }
    ]
  }),
  getters: {
    // 复杂 Getter:按分类筛选并计算总价(依赖 state.items)
    categoryTotalPrice(state) {
      console.log('categoryTotalPrice 重新计算') // 仅在 items 变化时打印
      // 按分类分组并计算每组总价
      return state.items.reduce((group, item) => {
        if (!group[item.category]) {
          group[item.category] = 0
        }
        group[item.category] += item.price * item.count
        return group
      }, {})
    }
  }
})

// 组件中使用
const cartStore = useCartStore()
console.log(cartStore.categoryTotalPrice) // 首次执行,打印“重新计算”,返回 { electronics: 5999, clothes: 598 }
console.log(cartStore.categoryTotalPrice) // 第二次执行,直接返回缓存结果,不打印
cartStore.items.push({ goodsId: 3, name: '裤子', price: 399, count: 1, category: 'clothes' })
console.log(cartStore.categoryTotalPrice) // items 变化,重新计算,打印“重新计算”,返回 { electronics: 5999, clothes: 997 }

缓存失效条件

  • 直接修改 Getter 依赖的 state 字段(如 cartStore.items.push(...));
  • 调用 Action 修改依赖的 state(如 cartStore.addItem(...));
  • Getter 依赖其他 Getter,且依赖的 Getter 缓存失效。
(2)参数化 Getter(实现动态查询)

Pinia Getter 本身不支持直接传参,但可通过“返回函数”实现参数化,满足“动态筛选”场景(如根据商品 ID 查询购物车商品、根据分类筛选列表)。

示例:参数化 Getter 查询购物车商品

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [
      { goodsId: 1, name: '手机', price: 5999, count: 1 },
      { goodsId: 2, name: '衬衫', price: 299, count: 2 }
    ]
  }),
  getters: {
    // 参数化 Getter:返回函数,接收 goodsId 作为参数
    getItemByGoodsId(state) {
      // 注意:返回函数会导致 Getter 缓存失效(每次调用都重新执行)
      // 优化方案:若参数范围固定,可提前缓存结果(如用 Map 存储)
      const itemMap = new Map(state.items.map(item => [item.goodsId, item]))
      return (goodsId) => itemMap.get(goodsId) || null
    },
    // 优化版参数化 Getter:缓存 Map,仅在 items 变化时重建 Map
    getItemByGoodsIdOptimized(state) {
      // 第一步:计算依赖的缓存数据(仅在 items 变化时执行)
      const itemMap = new Map(state.items.map(item => [item.goodsId, item]))
      // 第二步:返回查询函数(复用 itemMap,避免每次重建)
      return (goodsId) => itemMap.get(goodsId) || null
    }
  }
})

// 组件中使用
const cartStore = useCartStore()
console.log(cartStore.getItemByGoodsIdOptimized(1)) // { goodsId: 1, ... }
console.log(cartStore.getItemByGoodsIdOptimized(2)) // { goodsId: 2, ... }
(3)Getter 组合(复用衍生逻辑)

多个 Store 的 Getter 若存在重复逻辑(如“分页计算”“价格格式化”),可通过“抽取公共函数”或“Getter 调用其他 Getter”实现组合复用。

示例:抽取公共分页 Getter 逻辑

// src/utils/storeHelpers.js(公共 Getter 工具函数)
export const createPaginationGetter = (stateKey) => {
  // 返回分页 Getter 函数,接收 page(页码)和 pageSize(页大小)
  return function (page = 1, pageSize = 10) {
    const list = this[stateKey] // this 指向 Store 实例
    const startIndex = (page - 1) * pageSize
    const endIndex = startIndex + pageSize
    return {
      list: list.slice(startIndex, endIndex), // 当前页数据
      total: list.length, // 总条数
      totalPages: Math.ceil(list.length / pageSize) // 总页数
    }
  }
}

// src/store/goodsStore.js(商品 Store 使用公共分页逻辑)
import { defineStore } from 'pinia'
import { createPaginationGetter } from '@/utils/storeHelpers'

export const useGoodsStore = defineStore('goods', {
  state: () => ({
    goodsList: [] // 商品列表(

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐