今天还是说移动app开发,店铺系列文章,我们经常去超市使用购物车,即一个临时的储物空间,用完清空回收。我大兄弟说,
平时很忙,录入订单的目录很多,临时有事回来要可以继续填写,提交订单后才算结束,这就是一个典型的购物车场景了。那
系统的购物车如何实现?现在就来实战一把,做个如淘宝类的购物车。
作者原创文章,谢绝一切转载!
本文只发表在"公众号"和"博客园",其他均属复制粘贴!如果觉得排版不清晰,请查看公众号文章。
准备:
Idea2019.03/Gradle6.0.1/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/Dubbo2.7.5/Mysql8.0.11
/Vue2.5/OSS/Hbuilder2.6.1
难度: 新手--战士--老兵--大师
目标:
- 手机APP前端实现购物车功能
- async/await使用
步骤:
为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。代码地址:https://github.com/xiexiaobiao/vehicle-shop-mobile.git
1 本套系统大体情况
后端代码量约1.5万,双前端约1.5万,技术还是很具代表性的,不然就不好意思拿出来说事了,详细可看Git库说明,下图是后端代码量分析:
Web管理界面:需要密码的,请公众号留言。
手机端:使用Hbuilder编码,Uniapp框架,再随手捡了几个UI拿来大改了几下,基本形状如下:
2 购物车原理
先说存储,有三套方案;一是直接数据库端存储,与后台交互多,会增加流量和业务复杂度;二是Localstorage存储,持久化到本地浏览器端,除非主动
删除,否则永久存在;三是Session级别存储,使用vuex组件,会话级存储,app关闭即清空。再说vuex组件,是vue框架组件之一,其最常用的功能就是
存储用户登录状态,因为系统很多地方的使用都需要进行登录验证,我们可以在用户登录之后,将登录状态写入vuex,那系统其他地方就可以随用随取,
我这里即说第三套方案,使用vuex做缓存实现购物车。Vuex基础知识,略!请君自查!
思路:建立一个vuex数组,即购物车存储空间,选择商品后,即加入该数组中,如果数量等属性有更新,也同步到该该数组,只要app不关闭就可以打开
购物车继续编辑,直到提交订单时清空该数组。
3 购物车存储
vehicle-shop-app/store/index.js
import Vue from'vue' import Vuex from'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { hasLogin: false, userInfo: {}, // session周期有效 items: [{ idItem: 3, itemUuid: 'SP100034', category: '保养', classification: '', itemName: '特色全合成机油', sellPrice: 160.00, discountPrice: 150.00, brandName: '丰田', description: '1.5升塑料瓶装', shipment: true, quantity: 3, remark: '八折优惠5块钱', alertQuantity: 5, specification: '1.5升瓶装', unit: '瓶', sales: 20 , stock: 50, checked: true, // 是否选中 picAddr:'http://biao-aliyun-oss-pic-bucket.oss-cn-shenzhen.aliyuncs.com/images/2020/03/08/1583628752948gv86t511pi.jpg', }, ], }, // 同步操作 mutations: { login(state, provider) { state.hasLogin = true; state.userInfo = provider; // 将数据存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个异步接口 // 对比vuex,localstorage是永久存储,保存在本地浏览器中 uni.setStorage({//缓存用户登陆状态 key: 'userInfo', data: provider }) console.log(state.userInfo); }, logout(state) { state.hasLogin = false; state.userInfo = {}; uni.removeStorage({ key: 'userInfo' }) }, // 添加进购物车 addCartItems(state,provider){ const cartItem = state.items.find(item => item.itemUuid === provider.itemUuid) if(!cartItem){ state.items.push(provider); }else{ cartItem.quantity ++; } }, // 清空 emptyCart(state){ state.items = []; }, // 删除一个商品, 形参如果有多个,可使用{} deleteCartItem(state,idItem){ // 注意es6语法 findIndex 和 find 使用 let index = state.items.findIndex(item => item.idItem === idItem) state.items.splice(index,1); }, // 解构 incrementItemQuantity (state, { idItem }) { const cartItem = state.items.find(item => item.idItem === idItem) cartItem.quantity++; }, decrementItemQuantity (state, { idItem }) { const cartItem = state.items.find(item => item.idItem === idItem) cartItem.quantity--; }, setItemQuantity (state, {idItem,quantity }) { const cartItem = state.items.find(item => item.idItem === idItem) cartItem.quantity = quantity; }, }, // 异步 actions: { //// {commit} 解构 context对象,context与store实例具有相同的属性和方法。这里commit 就是 store.commit emptyCartAsync({commit}){ setTimeout(()=>{ commit("emptyCart"),3000}) }, addCartAsync: (context,provider) => { setTimeout(()=>{ context.commit('addCart',addCartItems),3000}) }, /* emptyCartAsync: context => { return context.commit('emptyCart') } */ }, getters:{ cartItems: state => { return state.items; } } }) exportdefault store
以上代码解析:
- 文件头引入 import Vuex from 'vuex'
- states区是类变量和初始值,定义一个items: []用于存放购物车商品,这里我直接写了一个商品先放里面,可以直观看到数据结构,也方面后面测试,
- mutations: {}中属于”同步”方法,包含一些购物车操作的方法,比如addCartItems(state,provider)是添加商品进购物车,我设计成允许重复添加,如果想不重复,直接返回不同代码即可。
- actions: {}是属于”异步”方法区,可以调用mutations: {}同步区的方法,也可自己写,
- getters和setters属于vuex基础,略!
4 全局声明
vehicle-shop-app/ main.js
中:
import Vue from'vue' import store from'./store'// 全局存储 import App from'./App' import Request from'./plugins/request/js/index' //测试用数据 import Json from'./Json' import report from'./pages/report/home.vue' Vue.component('report',report) //这里全局引入,并注册为vue组件,相比单页面js引入,使用更方便 /* import uniNavBar from "./components/uni-nav-bar/uni-nav-bar.vue" Vue.component('uniNavBar',uniNavBar) */ /* import cuCustom from './colorui/components/cu-custom.vue' Vue.component('cu-custom',cuCustom) */ import uniIcons from"@/components/uni-icons/uni-icons.vue" Vue.component('uniIcons',uniIcons) //设置全局的api地址 Vue.prototype.websiteUrl = 'http://10.4.14.132:7000'; const msg = (title, duration=1500, mask=false, icon='none')=>{ //统一提示方便全局修改 if(Boolean(title) === false){ return; } uni.showToast({ title, duration, mask, icon }); } const hidemsg = ()=>{ uni.hideToast()({ }); } const json = type=>{ // 模拟异步请求数据 returnnewPromise(resolve=>{ setTimeout(()=>{ resolve(Json[type]); }, 500) }) } const prePage = ()=>{ let pages = getCurrentPages(); let prePage = pages[pages.length - 2]; // #ifdef H5 return prePage; // #endif return prePage.$vm; } Vue.config.productionTip = false Vue.prototype.$fire = new Vue(); Vue.prototype.$store = store; Vue.prototype.$api = {msg, hidemsg, json, prePage}; Vue.prototype.$http = Request; App.mpType = 'app' const app = new Vue({ ...App }) app.$mount()
以上代码解析:
- import store from './store' 引入全局存储
- Vue.prototype.$store = store;这样,如果页面需要使用时,举例如下:
如果使用同步方法:this.$store.commit("deleteCartItem",itemIdToDel)
如果使用异步方法:this.$store.dispatch("addCartAsync",itemIdToAdd)
5 添加进购物车
vehicle-shop-app/pages/product/product.vue
商品详细页面:
js关键代码:
// 加入购物车 addCartItem(){ // vuex保存 this.$store.commit('addCartItems',this.product); uni.showToast({ title: "加购物车成功!", icon: 'info' }); },
6 购物车管理:
vehicle-shop-app/pages/order/cart.vue
这个物品就是前面vuex购物车默认的一个物品,
展示下JS部分的代码:
<script> import { mapGetters, mapState,mapActions,mapMutations } from'vuex' import uniNumberBox from'@/components/uni-number-box.vue' exportdefault { components: { uniNumberBox }, data() { return { total: 0, //总价格 allChecked: false, //全选状态 true|false empty: false, //空白页现实 true|false cartList: [], hasLogin: true, }; }, activated() { /* 解决 由订单页返回购物车页,购物车却为空的问题 */ /* 解决 由订单页返回购物车页,购物车却为空的问题 */ // 只要进入该页面就进行刷新,因为onLoad()只加载一次, // https://blog.csdn.net/qq_27047215/article/details/98943080 this.loadData(); }, onLoad(){ this.loadData(); }, watch:{ //显示空白页 cartList(e){ let empty = e.length === 0 ? true: false; if(this.empty !== empty){ this.empty = empty; } } }, computed:{ // ...mapState(['hasLogin']), ...mapGetters(['cartItems']) }, methods: { // 引入后可直接使用 ...mapActions(['emptyCartAsync','addCartAsync']), ...mapMutations(['addCartItems','emptyCart','deleteCartItem']), //自动计算折扣价 setDiscountPrice:function(item){ // item.discountPrice = }, //请求数据 loadData(){ // 从vuex中取缓存 // 这里因为cartItems放computed中,自动成为一个data, let list = this.cartItems; let cartList = list.map(item=>{ item.checked = true; return item; }); this.cartList = cartList; this.calcTotal(); //计算总价 }, //监听image加载完成 onImageLoad(key, index) { this.$set(this[key][index], 'loaded', 'loaded'); }, //监听image加载失败 onImageError(key, index) { this[key][index].image = '/static/errorImage.jpg'; }, navToLogin(){ uni.navigateTo({ url: '/pages/login/login-home' }) }, //选中状态处理 check(type, index){ if(type === 'item'){ this.cartList[index].checked = !this.cartList[index].checked; }else{ const checked = !this.allChecked const list = this.cartList; list.forEach(item=>{ item.checked = checked; }) this.allChecked = checked; } this.calcTotal(type); }, //数量 numberChange(data){ console.log(JSON.stringify(data)) // 修改缓存中的数量 this.$store.commit("setItemQuantity",{idItem:this.cartList[data.index].idItem,quantity:data.number }) this.cartList[data.index].quantity = data.number; this.calcTotal(); }, //删除 deleteCartItem(index){ let list = this.cartList; let row = list[index]; let id = row.id; // 删除vuex中对象 let itemIdToDel = this.cartList[index].id; // this.deleteCartItem(0); this.$store.commit("deleteCartItem",itemIdToDel) this.cartList.splice(index, 1); this.calcTotal(); uni.hideLoading(); }, //清空 clearCart(){ uni.showModal({ content: '清空购物车?', success: (e)=>{ if(e.confirm){ // vuex使用,引入map辅助函数后,可以直接使用,或者使用$store语法等效 this.emptyCart(); // this.$store.commit("emptyCart") // this.$store.dispatch("emptyCartAsync"); this.cartList = []; } } }) }, //计算总价 calcTotal(){ let list = this.cartList; if(list.length === 0){ this.empty = true; return; } let total = 0; let checked = true; list.forEach(item=>{ if(item.checked === true){ total += item.discountPrice * Number(item.quantity); }elseif(checked === true){ checked = false; } }) this.allChecked = checked; this.total = Number(total.toFixed(2)); }, //创建订单 createOrder(paidStatus){ let list = this.cartList; let goodsData = []; list.forEach(item=>{ if(item.checked){ goodsData.push({ attr_val: item.attr_val, number: item.quantity }) } }) this.cartList = []; // this.$api.msg('跳转下一页 sendData'); uni.navigateTo({ url: `/pages/order/createOrder?paidStatus=${JSON.stringify(paidStatus)}` }) } } } </script>
以上代码解析:
- activated(){}和onLoad(){}都包含了this.loadData()做页面数据加载,为什么?这是vue生命周期决定的,因为onLoad()只加载一次,系统会自动缓存页面内容,如果你跑到商品页添加商品再返回购物车页,购物车却不显示,activated可以让页面每次进来都刷新一次,这样,购物车里就能实时更新了!
- computed:{...mapGetters(['cartItems'])}中,这是vuex语法糖,
import { mapGetters, mapState,mapActions,mapMutations } from 'vuex'
之后,就可以直接使用'cartItems'变量了,系统会自动生成,看loadData()中就是let list = this.cartItems;
- 数量修改:
-
//数量 numberChange(data){ console.log(JSON.stringify(data)) // 修改缓存中的数量 this.$store.commit("setItemQuantity",{idItem:this.cartList[data.index].idItem,quantity:data.number }) this.cartList[data.index].quantity = data.number; this.calcTotal(); },
- 清空购物车方法clearCart(),这里演示了三种使用vuex的模式:一是配合import相关的map辅助函数,然后直接使用this.emptyCart(); 二是同步方法this.store.dispatch("emptyCartAsync"); 殊途同归!请君自选!
-
clearCart(){ uni.showModal({ content: '清空购物车?', success: (e)=>{ if(e.confirm){ // vuex使用,引入map辅助函数后,可以直接使用,或者使用$store语法等效 this.emptyCart(); // this.$store.commit("emptyCart") // this.$store.dispatch("emptyCartAsync"); this.cartList = []; } } }) },
这样,购物车打造完毕!只要用户不关闭app,打开购物车页面,里面商品就会存在,当然,别忘了,提交订单时,清空下购物车,
因为出了超市,购物车得还给人家,不能带回家!
7 async/await化异步为同步
前面一篇,说到后台请求数据都是异步的,处理不好就是页面渲染完毕,结果后台数据才过来,这就尴尬了。所以这里我举个例子解决
下这个问题:
vehicle-shop-app/pages/product/list.vue
async switchChange(item){ item.checked = !item.checked; // console.log(JSON.stringify(item)); if(item.checked){ // 获取商品详细 let requestItem={}; await Request().request({ url: 'stock/vehicle/stock/item/uid/'+ item.itemUuid, method: 'get', header: {}, params: {} }).then( res => { // 返回的对象,多一层data封装,故写为response.data requestItem = res.data; }).catch(err => { console.error('is catch', err) this.err = err; }) // 设置数量默认值 requestItem = Object.assign(requestItem,{ discountPrice: requestItem.sellPrice, }) requestItem.quantity = 1; //加入vuex缓存,commit是同步方法 // this.$store.commit('addCartItems',requestItem); this.toAddItemList.push(requestItem); //修改角标值 this.totalChecked += 1; this.setStyle(1,true,this.totalChecked); uni.showToast({ title: "选择商品成功!", icon: 'info', duration: 300 }); }else{ // this.$store.commit("deleteCartItem",item) // 删除临时数组中的值 let index = this.toAddItemList.findIndex(item=>item.itemUuid === requestItem.itemUuid); this.toAddItemList.splice(index,10); this.totalChecked -= 1; this.setStyle(1,true,this.totalChecked); uni.showToast({ title: "取消商品成功!", icon: 'info', duration: 300 }); } },
代码解析:以上代码中switchChange()
方法,用于响应商品勾选发生变化的,得先去后台找到这个数据,然后做处理,先使用 async
修饰,
说明这个方法是个异步的方法,然后对异步的部分使用await
修饰,这样,系统发起阻塞,只有await后面的部分运行完毕,才会继续运行后面的代码!
重点就是: await后面必须一定是返回Promise
对象,不管你是封装的函数还是代码块,否则写了await无效果!如果君想试试效果,建议多写几个
console.log(“A/B/C”)放不同位置,打印下,看谁先打印,就有印象了,其实async/await就是早期promise.then()语法的现代版本,
补充:
- 实际代码和页面很可能和我上面说到不一样,因为需求在变,我代码也一直在更新,我尽量保留代码痕迹。
全文完!
我的其他文章:
- 1 H5开发移动应用APP(店铺系列一)
- 2 阿里云平台OSS对象存储
- 3 Dubbo学习系列之十七(微服务Soul网关)
- 4 Docker部署RocketMQ
- 5 流式计算(五)-Flink 计算模型
只写原创,敬请关注