📝 业务背景:当后端把购物车的锅甩给前端
在很多轻量级电商、B2B订货系统或是外包项目中,为了节省服务器资源和开发成本,后端往往不会提供购物车的增删改查接口。这就意味着:购物车的数据流转、持久化存储、以及状态管理,全部需要前端自己扛下来。
如果只用简单的 uni.setStorageSync 存取,不仅页面间状态无法实时同步,还会面临极其致命的**"多用户数据串号"**问题。
今天分享一套大厂标准的纯前端购物车架构方案 ,利用 Vuex 结合本地缓存,彻底解决数据持久化与多账号隔离的痛点。
💡 核心架构思路
本套方案的核心原则是:Vuex 负责页面间的响应式联动,Storage 负责杀掉小程序后的数据持久化。
-
动态存储 Key :绝不能写死
CART_KEY,必须与当前登录的UserId绑定,实现多账号数据隔离。 -
存完整对象:因为后端不提供根据 ID 批量查询购物车的接口,前端必须把商品的图片、标题、价格等完整对象存入本地。
-
价格防篡改防御 :前端的购物车总价仅作"展示"用途,真正点击"提交订单"时,只向后端传递
商品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:购物车页面的状态映射
利用 mapState 和 mapGetters,页面渲染和计算属性 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 元。
终极防御策略:
-
购物车仅供展示 :前端算出来的
totalPrice只用来给用户看。 -
结算页把关 :到了结算页(提交订单接口),绝对不可以把前端计算的总价传给后端直接扣款!
-
真实校验 :向后端的
addOrder接口只传递[{ goodsId: 1, count: 2 }]。由后端拿着 ID 去数据库查询最新价格,如果发现商品已下架或库存不足,由后端抛出异常拦截交易。
总结: 这套纯前端的购物车方案,完美解决了无需后端干预下的状态同步、多账号隔离、数据持久化三大难题,非常适合敏捷开发和中小型项目。如果对你有帮助,欢迎点赞收藏!👇