uniapp + Vue3 Pinia 状态管理实战:跨端持久化与购物车完整案例

2026-06-30 0 703

用惯了 Vuex 的开发者切换到 uniapp 的 Vue3 模式后,大概率会犹豫:到底继续用 Vuex 还是换成 Pinia。我的建议很明确——如果项目还在早期,直接上 Pinia,它更轻、类型推断更友好,而且社区已经给出了可靠的持久化插件。但 uniapp 跨端特性会让存储介质变得复杂:H5 用 localStorage,小程序用 wx.setStorage,App 端还可能用 plus.storage。这篇文章会从搭建环境到完成一个完整的购物车功能,把 Pinia 在实践中可能踩的坑都过一遍。

一、项目初始化与 Pinia 安装

先用 HBuilder X 或 CLI 创建一个默认的 Vue3 模板项目。然后在项目根目录执行:

npm install pinia pinia-plugin-persistedstate

这里 pinia-plugin-persistedstate 是一个官方推荐的持久化插件,能自动把 store 数据写入本地存储,省去手动序列化的麻烦。官方文档提到它兼容 localStorage、sessionStorage,也可以通过自定义 serializer 适配其他存储。

main.js 里注册 Pinia:

import App from './App'
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersistedstate from 'pinia-plugin-persistedstate'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  pinia.use(piniaPersistedstate)
  app.use(pinia)
  return {
    app,
    pinia
  }
}

注意 uniapp 的入口文件不是直接创建 Vue 应用,而是通过 createSSRApp 暴露一个工厂函数。这个差异会导致一些“直接在 main.js 里挂载全局属性”的教程无法直接用,但 Pinia 的注册方式不受影响。

二、定义一个带持久化的购物车 Store

在项目根目录创建目录 uni_modules/cart/stores(我习惯把业务模块放到 uni_modules 里,方便复用),新建 cart.js

import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],            // 商品列表 { id, title, price, quantity, selected }
    lastSyncTime: null,
  }),
  
  getters: {
    totalCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
    
    totalPrice: (state) => state.items
      .filter(item => item.selected)
      .reduce((sum, item) => sum + item.price * item.quantity, 0)
      .toFixed(2),
    
    selectedItems: (state) => state.items.filter(item => item.selected),
  },
  
  actions: {
    addItem(product) {
      const existing = this.items.find(item => item.id === product.id)
      if (existing) {
        existing.quantity++
      } else {
        this.items.push({
          id: product.id,
          title: product.title,
          price: product.price,
          image: product.image,
          quantity: 1,
          selected: true,
        })
      }
      this.lastSyncTime = Date.now()
    },
    
    removeItem(id) {
      const index = this.items.findIndex(item => item.id === id)
      if (index > -1) {
        this.items.splice(index, 1)
        this.lastSyncTime = Date.now()
      }
    },
    
    updateQuantity(id, quantity) {
      const item = this.items.find(item => item.id === id)
      if (item && quantity > 0) {
        item.quantity = quantity
        this.lastSyncTime = Date.now()
      }
    },
    
    toggleSelect(id) {
      const item = this.items.find(item => item.id === id)
      if (item) {
        item.selected = !item.selected
      }
    },
    
    toggleAll(selected) {
      this.items.forEach(item => { item.selected = selected })
    },
    
    clearCart() {
      this.items = []
      this.lastSyncTime = null
    }
  },
  
  // 持久化配置
  persist: {
    storage: {
      getItem: (key) => {
        // 多端兼容读取
        if (typeof uni !== 'undefined') {
          return uni.getStorageSync(key)
        }
        return localStorage.getItem(key)
      },
      setItem: (key, value) => {
        if (typeof uni !== 'undefined') {
          uni.setStorageSync(key, value)
        } else {
          localStorage.setItem(key, value)
        }
      },
    },
    serializer: {
      serialize: JSON.stringify,
      deserialize: JSON.parse,
    },
    // 只持久化 items,不存时间戳(减少存储大小)
    paths: ['items'],
  }
})

这个 store 的 persist 配置是整个方案的核心。通过自定义 storage 对象,我们统一了 H5、小程序和 App 的存储入口:在 uniapp 环境中调用 uni.setStorageSyncuni.getStorageSync,否则降级到 localStorage。这样写的好处是即使是纯 Vue 项目里也可以复用同一套 store 代码。

三、页面中使用购物车

pages/cart/cart.vue 中引用 store:

<template>
  <view class="cart">
    <view v-if="cartStore.items.length === 0" class="empty">购物车是空的</view>
    <view v-else>
      <view class="cart-header">
        <checkbox :checked="allSelected" @tap="toggleAll(!allSelected)">全选</checkbox>
        <text class="total">合计:¥{{ cartStore.totalPrice }}</text>
        <button size="mini" @tap="clearCart">清空</button>
      </view>
      
      <view class="item" v-for="item in cartStore.items" :key="item.id">
        <checkbox :checked="item.selected" @tap="cartStore.toggleSelect(item.id)"></checkbox>
        <image :src="item.image" mode="aspectFill"></image>
        <view class="info">
          <text>{{ item.title }}</text>
          <text>¥{{ item.price }}</text>
          <view class="quantity">
            <button size="mini" @tap="cartStore.updateQuantity(item.id, item.quantity - 1)">-</button>
            <text>{{ item.quantity }}</text>
            <button size="mini" @tap="cartStore.updateQuantity(item.id, item.quantity + 1)">+</button>
          </view>
        </view>
        <button size="mini" @tap="cartStore.removeItem(item.id)">删除</button>
      </view>
    </view>
  </view>
</template>

<script setup>
import { useCartStore } from '@/uni_modules/cart/stores/cart'
import { computed } from 'vue'

const cartStore = useCartStore()

const allSelected = computed(() => {
  return cartStore.items.length > 0 && cartStore.items.every(item => item.selected)
})

const toggleAll = (selected) => {
  cartStore.toggleAll(selected)
}

const clearCart = () => {
  uni.showModal({
    title: '提示',
    content: '确定清空购物车吗?',
    success: (res) => {
      if (res.confirm) cartStore.clearCart()
    }
  })
}
</script>

这里没有任何手动调用 saveload,因为持久化插件会在状态改变后自动写入存储,应用启动时自动读取。开发者只需要像操作普通对象一样修改 state。

四、解决跨端持久化的兼容性陷阱

  • 小程序存储容量限制:单条数据不能超过 1MB,总存储不能超过 10MB。如果购物车数据量很大,建议只持久化必要字段(上面的 paths: ['items'] 已经做了裁剪),并把商品图片等大体积资源存到云端。
  • App 端 plus.storage:uni 的 setStorageSync 在 App 端内部会映射到 plus.storage,完全兼容。如果没有 uni 对象,我们的自定义存储会回退到 localStorage,这在本地开发 H5 时也能正常运行。
  • 还原时类型丢失:JSON 序列化不能还原 Date 对象,所以 lastSyncTime 我存的是时间戳数字,而非 Date 实例。在 getter 或使用时再转成 Date。
  • 多标签页同步:H5 端如果打开了多个标签页,localStorage 的修改不会实时同步到其它标签页的 Pinia 实例。如果需要强同步,可以监听 storage 事件,在事件回调中调用 store 的 $patch 方法更新状态。这部分不是每个项目都需要,根据实际场景决定。

五、从 Vuex 迁移到 Pinia 的实操对比

如果你的项目还在用 Vuex,迁移成本并不高。以购物车为例,原来的 Vuex module 可能长这样:

// Vuex 版本(旧)
const state = { items: [] }
const mutations = {
  ADD_ITEM(state, product) { /* ... */ },
  REMOVE_ITEM(state, id) { /* ... */ },
}
const actions = {
  addToCart({ commit }, product) { commit('ADD_ITEM', product) },
}

在 Pinia 中,mutation 和 action 合并为 actions,直接通过 this 修改 state,不需要 commit。把 Vuex 的 state、getters、actions 复制到 Pinia 的对应位置,然后去掉 context 参数,改掉 state.xxxthis.xxx,一个模块就移完了。单文件量比原来少 30% 左右,而且 TypeScript 支持更好。

Vuex 的持久化插件 vuex-persistedstate 与 Pinia 插件用法类似,但后者允许在 store 内部配置 persist,不需要在入口文件统一配置,颗粒度更细。

六、性能与调试建议

  • 避免频繁写存储:持久化插件默认在每次 state 变化时都序列化整个 store 写入存储。如果某个 store 更新频率很高(比如陀螺仪数据),应当将 persist 设为 false,或者只持久化一个计算后的摘要值。
  • 利用 uni 的调试工具:在 HBuilder X 中可以在内置浏览器控制台直接查看 Pinia store 的状态,或者使用 uni.getStorageInfoSync() 检查已存储的键和大小。
  • 插件执行顺序:如果多个 store 互相引用,注意持久化插件只会在当前 store 完成修改后触发存储,不会导致死循环。但避免在 getter 里触发 action,可能引起堆积。

七、总结

Pinia 在 uniapp 项目里可以说完全替代了 Vuex,而且跨端持久化的定制方案很灵活。本文的购物车 store 代码可以直接用到实际项目中,通过自定义 storage 抹平了多端差异。当下一个 uniapp 新项目启动时,建议从一开始就选择 Pinia,它会让你在管理全局状态时少写不少模板代码,同时持久化这件事也变成了一种声明式配置,顺手得不像话。

uniapp + Vue3 Pinia 状态管理实战:跨端持久化与购物车完整案例
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp + Vue3 Pinia 状态管理实战:跨端持久化与购物车完整案例 https://www.taomawang.com/web/uniapp/2298.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务