UniApp 纯前端实现企业级购物车:Vuex + Storage 多用户状态管理闭环方案

📝 业务背景:当后端把购物车的锅甩给前端

在很多轻量级电商、B2B订货系统或是外包项目中,为了节省服务器资源和开发成本,后端往往不会提供购物车的增删改查接口。这就意味着:购物车的数据流转、持久化存储、以及状态管理,全部需要前端自己扛下来。

如果只用简单的 uni.setStorageSync 存取,不仅页面间状态无法实时同步,还会面临极其致命的**"多用户数据串号"**问题。

今天分享一套大厂标准的纯前端购物车架构方案 ,利用 Vuex 结合本地缓存,彻底解决数据持久化与多账号隔离的痛点。


💡 核心架构思路

本套方案的核心原则是:Vuex 负责页面间的响应式联动,Storage 负责杀掉小程序后的数据持久化。

  1. 动态存储 Key :绝不能写死 CART_KEY,必须与当前登录的 UserId 绑定,实现多账号数据隔离。

  2. 存完整对象:因为后端不提供根据 ID 批量查询购物车的接口,前端必须把商品的图片、标题、价格等完整对象存入本地。

  3. 价格防篡改防御 :前端的购物车总价仅作"展示"用途,真正点击"提交订单"时,只向后端传递 商品ID数量,以数据库最新价格为准。


🛠️ 第一步:打造购物车的"大脑" (Vuex 模块设计)

store/modules/ 目录下新建 cart.js。这里封装了购物车所有的核心业务逻辑。

javascript 复制代码
// store/modules/cart.js

// 🌟 核心点1:动态获取 Key,防止张三看到李四的购物车
const getCartKey = () => {
  const userInfo = uni.getStorageSync('userInfo') || {};
  const userId = userInfo.id || 'guest'; 
  return `MALL_CART_DATA_${userId}`; 
}

export default {
  namespaced: true,
  state: {
    list: [] // 初始化为空,等待 App 启动或登录后拉取
  },
  
  getters: {
    // 获取购物车总件数 (用于 TabBar 角标)
    totalCount: state => state.list.reduce((total, item) => total + item.count, 0),
    // 获取已勾选商品总价 (用于底部结算栏)
    totalPrice: state => {
      let sum = state.list.reduce((total, item) => {
        return item.selected ? total + (item.price * item.count) : total;
      }, 0);
      return sum.toFixed(2);
    },
    // 获取已勾选商品列表 (传递给结算页)
    selectedList: state => state.list.filter(item => item.selected),
    // 是否全选
    isAllSelected: state => state.list.length > 0 && state.list.every(item => item.selected)
  },
  
  mutations: {
    // 初始化当前用户的购物车
    INIT_CART(state) {
      state.list = uni.getStorageSync(getCartKey()) || [];
    },

    // 加入购物车
    ADD_TO_CART(state, goods) {
      const existIndex = state.list.findIndex(item => item.id === goods.id);
      if (existIndex > -1) {
        state.list[existIndex].count += (goods.count || 1);
      } else {
        // 新商品默认打勾
        state.list.unshift({ ...goods, count: goods.count || 1, selected: true });
      }
      uni.setStorageSync(getCartKey(), state.list); // 数据变动,立即落盘
    },

    // 切换选中状态
    TOGGLE_SELECT(state, goodsId) {
      const target = state.list.find(item => item.id === goodsId);
      if (target) {
        target.selected = !target.selected;
        uni.setStorageSync(getCartKey(), state.list);
      }
    },

    // 步进器修改数量
    UPDATE_COUNT(state, { id, count }) {
      const target = state.list.find(item => item.id === id);
      if (target) {
        target.count = count;
        uni.setStorageSync(getCartKey(), state.list);
      }
    },

    // 清空内存数据 (退出登录时调用)
    CLEAR_STATE(state) {
      state.list = [];
    }
  }
}

🔄 第二步:触发生命周期,严防"数据串号"

由于我们的 CART_KEY 是跟着 UserId 走的,所以必须在用户身份发生变化时,主动唤醒或休眠购物车。

1. 在 App 冷启动时 (App.vue):

javascript 复制代码
onLaunch() {
  const token = uni.getStorageSync('token');
  if (token) {
    this.$store.commit('cart/INIT_CART');
  }
}

2. 在用户登录成功时:

javascript 复制代码
// login.vue
loginSuccess() {
  // 保存完 userInfo 之后...
  this.$store.commit('cart/INIT_CART'); // 唤醒该用户的私有购物车
}

3. 在用户退出登录时:

javascript 复制代码
logout() {
  this.$store.commit('cart/CLEAR_STATE'); // 清除 Vuex 内存,防止下一个用户直接看到
}

🚀 第三步:无缝接入业务页面

有了强大的底层支撑,业务页面的代码变得极其清爽。

场景 A:商品详情页点击"加入购物车"

javascript 复制代码
methods: {
  handleAddToCart() {
    const cartItem = {
      id: this.goodsInfo.id,
      title: this.goodsInfo.productName,
      price: this.goodsInfo.productPrice,
      image: this.goodsInfo.productPic,
      count: 1
    };
    this.$store.commit('cart/ADD_TO_CART', cartItem);
    uni.showToast({ title: '已加入购物车' });
  }
}

场景 B:购物车页面的状态映射

利用 mapStatemapGetters,页面渲染和计算属性 0 延迟,极度丝滑。

javascript 复制代码
<template>
  <view class="bottom-bar">
    <text>合计: ¥{{ totalPrice }}</text>
    <button @click="goToSettle">去结算({{ totalCount }})</button>
  </view>
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
  computed: {
    ...mapState('cart', ['list']),
    ...mapGetters('cart', ['totalPrice', 'totalCount', 'selectedList'])
  },
  methods: {
    ...mapMutations('cart', ['TOGGLE_SELECT', 'UPDATE_COUNT']),
    goToSettle() {
      if (this.selectedList.length === 0) return uni.showToast({ title: '请选择商品' });
      // 将 selectedList 传递给结算页即可
    }
  }
}
</script>

⚠️ 避坑指南(进阶防御)

既然前端接管了购物车数据库,就必须承担**"本地数据与服务器数据不一致"**的风险。比如用户把一件 99 元的衣服加入购物车,第二天商家涨价到了 199 元,或者商品下架了,此时本地缓存里存的依然是 99 元。

终极防御策略:

  1. 购物车仅供展示 :前端算出来的 totalPrice 只用来给用户看。

  2. 结算页把关 :到了结算页(提交订单接口),绝对不可以把前端计算的总价传给后端直接扣款!

  3. 真实校验 :向后端的 addOrder 接口只传递 [{ goodsId: 1, count: 2 }]。由后端拿着 ID 去数据库查询最新价格,如果发现商品已下架或库存不足,由后端抛出异常拦截交易。


总结: 这套纯前端的购物车方案,完美解决了无需后端干预下的状态同步、多账号隔离、数据持久化三大难题,非常适合敏捷开发和中小型项目。如果对你有帮助,欢迎点赞收藏!👇

相关推荐
浮桥2 小时前
uniapp页面列表列表请求hook记录
前端·javascript·uni-app
bluceli2 小时前
前端微前端架构实战指南:构建可扩展的大型应用架构
前端
阿懂在掘金2 小时前
Vue Asyncx 库三周年,回顾起源时的三十行代码
前端·typescript·开源
一只不会编程的猫2 小时前
Echart 3D环形图
前端·javascript·3d
脸大是真的好~2 小时前
黑马AI+前端教程 01-HTML-Trae-F12-live Server-标签-块级和内联元素-图片格式-路径
前端·html
楚城相拥2 小时前
uniapp引入bmob实现预览
uni-app
清空mega2 小时前
《学 Vue3 前需要掌握什么基础?HTML、CSS、JavaScript 与 ES6 一次讲清》
状态模式
前端付豪2 小时前
拍照识题 OCR
前端·后端·python
专业流量卡2 小时前
龙虾写useEffect源码第二天
前端