尚硅谷Vue电商项目笔记5-购物车的实现和Promise的回顾
实现Detail商品数量的增加和减少
注意:
- 用来输入数量的是input,用户可以自己输入数量,所以要对用户输入的数量进行逻辑操作,防止他们输入错误的内容
- 输入框的blur事件,是当失去焦点的时候触发,但是这个事件不好,因为只要输入焦点就会触发该事件,而不能判断这次输入的数据和之前的数据是否一样才会去触发
- 使用change事件,该事件包含了失去焦点事件,当输入框失去焦点的时候,如果里面的数据和之前的不一样才会触发,否则不会触发
- 当时去焦点的时候使用
$event.target.value
判断里面输入的数字,如果不合法就将skuNum设置为1 - 当失去焦点的时候合法,就使用用户输入的数字
- 当时去焦点的时候使用
<div class="controls">
<!-- 当用户输入一个非数字,则转换为nan,还是会走skuNum=1,并且这里将收集到的字符串使用*1,转换为了数字 -->
<input autocomplete="off" class="itxt" v-model="skuNum" @change="$event.target.value >=1 ? skuNum = $event.target.value*1 : skuNum=1">
<a href="javascript:" class="plus" @click="skuNum++">+</a>
<a href="javascript:" class="mins" @click="skuNum > 1 ? skuNum-- : skuNum = skuNum">-</a>
</div>
点击添加购物车按钮跳转到添加成功页面
特别的地方:
- 之前的跳转都是点击直接跳转,因为在跳转之前不需要发送请求,而是跳转过去之后发送请求后去数据
- 但是加入购物车,需要先将请求发给后台,后台需要把这个购物车信息存储到数据库,请求成功返回信息,在根据信息去跳转,否则可能后台添加数据库失败,但是我们还是跳转到购物车页面,这样就会出问题
操作步骤:
- 根据接口书写接口请求函数,需要两个参数,一个是skuId,一个是skuNum
- 将需请求成功的购物车组件拷贝到pages目录下
- 该组件是路由组件,所以进行路由注册
- 创建新的模块shopcart专门存放购物车相关的数据,写vuex,不要忘记将模块化的vuex最终合并到主文件中
async函数:
- 该函数被称作异步函数,一般内部都是有异步操作的
- 该函数的返回值一定是promise实例对象,不看return
- return的结果如果是非promise对,那么该promise的状态一定式成功的,成功的结果就是return的结果
- return的结果如果是promise对象,那么要看这个return后面的promise对象是成功的还是失败的
- 如果return的promise对象是成功的,那么promise对象就是成功的,成功的结果就是return的promise的成功结果
- 如果return的promise对象是失败的,那么promise对象就是失败的,失败的原因就是return的promise的失败原因
- 如果没有return结果而是抛出错误,promise也是失败的,原因就是抛出的错误原因
请求接口函数:
// 添加购物车的请求接口函数:/api/cart/addToCart/{ skuId }/{ skuNum }
export const reqAddOrUpdateShopCart = (skuId,skuNum)=>{
return request({
url:`/cart/addToCart/${ skuId }/${ skuNum }`,
method:'post'
})
}
路由配置:
{
path:'/saddcartsuccess',
component:AddCartSuccess
},
vuex文件:
- 该文件只有actions函数,因为不需要存储数据
- 因为dispatch的时候,只能传递两个参数,所以需要携带的参数skuId和skuNum需要通过对象的形式传递,在接收的时候也需要对象的形式接收
- async如果返回的是字符串,则返回的永远都是成功状态的promise实例对象,所以当请求失败的时候,这里返回了一个失败的promise对象
import {reqAddOrUpdateShopCart} from '@/api'
const state = {}
const mutations = {}
const actions = {
// 这个发送请求的函数比较特殊,因为没有获取数据,转交给mutations来存储数据
// dispath只能传递两个参数,一个是要派发的任务,另一个就是对象,所以这里多个参数,提前使用解构赋值拿出来
async addOrUpdateShopCart({commit},{skuId,skuNum}){
const result = await reqAddOrUpdateShopCart(skuId,skuNum)
// 这种写法是可以的,但是async函数返回的promise将永远成功,不符合常理,promise实例如果返回非promise实例,则该promise实例的状态永远都是成功的
// if(result === 200){return 'ok'} else {return 'failed'}
if(result.code === 200){
return 'ok'
}else{
return Promise.reject(new Error('failed'))
}
},
}
const getters = {}
export default {
state,mutations,actions,getters
}
跳转到购物车的回调函数:
- 执行dispatch的时候,使用try-catch的形式来处理可能失败的情况
- 在传递参数的时候,因为只能两个参数所以第二个参数以对象的形式传递,千万不要忘记对象是键值对的形式
- 向后台发送请求添加购物车是否成功的时候携带的参数跟跳转到成功购物车页面需要的参数不一样,购物车页面要的是skuNum(数量)和skuInfo(详情)
- 数量直接通过query参数传递
- 但是详情比较复杂是一个对象,所以利用本地存储的形式,但是在传递对象的时候不要忘记使用
JSON.stringify
获取的时候使用JSON.parse
注意:这里使用的是async函数
async addShopCart(){
// 第一步发送请求添加购物车
try {
// 因为addOrUpdateShopCart函数是async函数,所以执行,有可能两种结果,一个是成功的promise实例,一个是失败的promise实例
await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.skuId,skuNum:this.skuNum})
alert('添加购物车成功,准备跳转到购物车页面')
// 第二步:根据请求添加购车返回信息决定是否跳转到添加购物车成功页面
// 跳转到购物车页面要携带两个东西,一个是数量,一个是商品的详情,数量直接通过参数带过去,详情因为是复杂信息存储到本地
this.$router.push('/saddcartsuccess?skuNum='+this.skuNum)
// sessionStorage浏览器关闭消失,localStorage一直存在
sessionStorage.setItem('SKUINFO_KEY',JSON.stringify(this.skuInfo))
} catch (error) {
alert(error.message)
}
}
添加购物车成功之后,在成功页面展示数据
- 在
data
中定义需要的数据 - 在
beforeMount
中将数据获取出来 - 使用数据展示数量,展示名字,展示图片即可
- 解决图标显示问题:将需要的
iconfont.css
和fonts字体目录拷贝到public下就可以正常工作,因为iconfont.css
在reset.css
中已经引入过了 - 使用
<router-link>
替换<a>
实现跳转到详情页面,跳转到详情需要一个params参数,表示是那种商品,然后再Detail组件中,mounted
会发送请求获取数据
添加购物车成功之后获取数据:
export default {
name: 'AddCartSuccess',
data(){
return {
skuNum:'',
skuInfo:{}
}
},
beforeMount(){
this.skuNum = this.$route.query.skuNum
this.skuInfo = JSON.parse(sessionStorage.getItem('SKUINFO_KEY'))
}
}
重新跳转到详情页面:
<router-link :to="`/detail/${skuInfo.id}`" class="sui-btn btn-xlarge">查看商品详情</router-link>
购物车页面
从购物车成功页面跳转到购物车结算页面
- 使用提供的路由组件ShopCart拷贝到pages下,然后配置路由信息,点击去购物车结算使用
<router-link
>进行跳转 - 调整静态页面的css结构,百分比分别是:15,35,10,17,10,13
跳转到购物车结算页面:
<router-link to="/shopcart" >去购物车结算 > </router-link>
获取数据展示结算页面
- 编写请求接口函数
- 引入请求接口函数,编写Vuex获取数据
- ShopCart组件挂载后从Vuex中获取数据
请求接口函数别忘记暴露出去:
// 获取购物车信息的请求接口函数:/api/cart/cartList
export const reqShopCartInfo = ()=>{
return request({
url:'/cart/cartList',
method:'get'
})
}
将派发请求定义成一个方法,这样以后只需要调用请求就可以发送请求获取数据:
export default {
name: 'ShopCart',
methods:{
// 派发请求定义成一个方法,方便之后使用
getShopCartInfo(){
this.$store.dispatch('getShopCartInfo')
}
},
mounted(){
this.getShopCartInfo()
}
}
问题:通过network中查看,请求发送成功,但是数据中data:[]
- 添加购物车的时候向数据库存储数据,但是没有给数据库一个标识
- 在获取数据的时候不知道该获取谁的
生成临时标识
注意:临时标识就是用户没有登陆的时候使用的标识,临时标识在请求拦截器中,添加到请求头中,这样在访问后台的时候,每次请求都会携带这个标识
- 在store目录下创建一个user模块,该模块负责存储用户临时标识信息,引入工具方法生成uuid,然后存储在state中(不要忘记将该模块引入主模块)
// 引入和用户相关的工具包
import { getUserTempId } from '@/utils/userabout'
const state = {
userTempId: getUserTempId()
}
const mutations = {}
const actions = {}
const getters = {}
export default {
state,mutations,actions,getters
}
- 创建utils目录,在该目录下创建userabout.js定义和用户相关的工具函数
// uuid是一个依赖包,不需要下载,直接引入即可
import { v4 as uuidv4 } from 'uuid'
// 这个函数专门用来生成用户的唯一标识,每个浏览器拥有的都是相同的,因为存储到了lcoalStorage中
export const getUserTempId = ()=>{
// 先从localStorage获取
let userTempId = localStorage.getItem('USERTEMPID_KEY')
// 如果没有获取到则返回null
if(!userTempId){
// 如果没有我就自己生成一个uuid,然后存储起来,这样以后获取都是从localStroage中获取,就获取到的都是一样的了
userTempId = uuidv4()
localStorage.setItem('USERTEMPID_KEY',userTempId)
}
// 需要存储到仓库中,所以返回出去
return userTempId
}
- 在封装的axios中请求拦截器中添加请求头,这样就能在跳转到购物车页面获取到数据
// 如果需要给axios添加额外的功能,或者给请求头添加额外的信息,必须使用到请求拦截器和响应拦截器
server.interceptors.request.use((config) => { // 请求拦截器也有一个失败的回调,但是一般没有用,因为失败了就没有下文了
// 在发送请求之前从大仓库中查看是否有唯一的userTempId,注意这里是一个模块,而不是组件,所以不能直接使用this.$store,所以要引入store
const userTempId = store.state.user.userTempId
if(userTempId){
// 添加的请求头必须是userTempId
console.log(userTempId)
config.headers.userTempId = userTempId
}
// 添加额外的功能,或者添加特殊的请求头,这里执行nprogress的start
nprogress.start()
return config // 将配置对象返回,否则请求无法真正发出
})
购物车页面的数据展示
- 使用
getters
简化从Vuex中获取数据 - 因为数据结构很奇怪,所以使用计算属性继续简化出来要遍历的数据
- 展示数据:计算总价,计算已经选择的商品,计算全选,注意:全选按钮,没有指定vlaue,也没有使用数组接收,那么默认收集的是checked,所以全选按钮使用v-model
全选按钮的绑定:
<div class="select-all">
<!-- 使用v-mode进行双向数据绑定,在多选框中,指定了value然后使用数组接收,但是,对于checkbox,是单个的,v-model没有使用数组接收,则收集的是checked属性即布尔值,而不是value -->
<input class="chooseAll" type="checkbox" v-model="isCheckAll" />
<span>全选</span>
</div>
计算属性需要计算的值:
// shopcartVuex中使用getters简化数据获取
const getters = {
cartInfo(state){
// 获取到数组的第一项,只有一项,然后避免假报错
return state.shopCartInfo[0] || {}
}
}
// 在ShopCart中使用计算属性获取
computed: {
...mapGetters(["cartInfo"]),
// 继续计算出要遍历的数组
cartInfoList() {
// 同样要避免假报错
return this.cartInfo.cartInfoList || [];
},
// 计算已经选中的商品
checkedNum() {
// 第一个return是返回的值
return this.cartInfoList.reduce((prev, item) => {
// 被选中为1,没有选中为0,就是false
if (item.isChecked) {
// 这个是reduce中满足条件的return
prev += item.skuNum;
}
return prev
}, 0);
},
// 计算总价
allMoney() {
return this.cartInfoList.reduce((prev, item) => {
if (item.isChecked) {
prev += item.skuNum * item.skuPrice;
}
return prev
}, 0);
},
// 计算全选是否选中,这个选中是可以修改的,所以使用完整写法
isCheckAll: {
// 读方法
get() {
return this.cartInfoList.every((item) => {
// 每一项都被选中的时候,则全选为true,只要有一项方法false,则结果为false
return item.isChecked
});
},
// 写方法
set(){
}
},
},
修改购物车数据
购物车交互分析
- 用户购物车展示的数据都是从数据库获取的,在购物车页面修改信息,需要修改数据库的数据
- 数据库的数据修改之后,ShopCart组件重新发送请求,然后拿到新的数据,重新展示
注意:所以购物车的页面只要修改就要向后台发送请求
修改购物车商品数量
接口请求函数,这个接口请求函数已经写过了就是添加购物车的接口
- 接口传递skuNum是正数则代表的是增加的数量
- 接口传递skuNum是负数则代表的是减少的数量
要注意:
- 点击+或者-按钮,传递的值时变化的量
- 而直接在输入框输入,得到的是最终量
给按钮和输入框添加事件:
<li class="cart-list-con5">
<a
href="javascript:void(0)"
class="mins"
@click="changeCartNum(cartInfo, -1, true)"
>-</a
>
<input
autocomplete="off"
type="text"
:value="cartInfo.skuNum"
minnum="1"
class="itxt"
@change="changeCartNum(cartInfo, $event.target.value * 1, false)"
/>
<a
href="javascript:void(0)"
class="plus"
@click="changeCartNum(cartInfo, 1, true)"
>+</a
>
</li>
数量修改的回调函数:
// 购物车数量的方法
// 第一个参数就是购物车展示的对象,disNum如果点击的是加号和减号则为1和-1,第三个参数标记是点击的还是输入的
// 如果点击按钮得到是变化的量,如果是失去焦点进来的则disNum是最终的数量
async changeCartNum(cartInfo, disNum, flag) {
// 原来的值
let originNum = cartInfo.skuNum;
// 触发按钮进来的
if (flag) {
// 小于1的情况就是有1,然后点击了-1,然后比1小,就修正,传递过来的内容,显示的结果最终为1,就是originNum+disNum = 1
if (originNum + disNum < 1) {
disNum = 1 - originNum;
}
} else {
// 用户输入进来的
if (disNum < 1) {
// 如果用户输入最终量小于1,就让最终量等1,那么disNum + originNum = 1,让输入的变成计算出的数字,最后的结果就是1
disNum = 1 - originNum;
} else {
disNum = disNum - originNum;
}
}
// 到这里disNum就是我们需要的增量,发送请求就跟添加到购物车是一样的,使用try-catch处理
try {
await this.$store.dispatch("addOrUpdateShopCart", {
skuId: cartInfo.skuId,
skuNum: disNum,
});
alert('修改成功')
this.getShopCartInfo();
} catch (error) {
alert(error.message);
}
},
修改购物车的选中状态单个
- 写请求接口函数
- 写Vuex发送请求
- 添加事件发送请求,根据原来的选中状态决定修改的选中状态
接口请求函数:
// 修改购物车的选中状态:/api/cart/checkCart/{skuID}/{isChecked},isChecked为0表是不选中,isChecked为1表示选中
export const reqUpdateCartIscheck = (skuId,isChecked)=>{
return request({
url:`/cart/checkCart/${skuId}/${isChecked}`,
method:'get'
})
}
Vuex中的发送请求:
// 修改购物车的选中状态
async updateCartIscheck({commit},{skuId,isChecked}){
const result = await reqUpdateCartIscheck(skuId,isChecked)
if(result.code === 200){
return 'ok'
}else{
return Promise.reject(new Error('checked change failed'))
}
}
点击选框的回调函数:
async updateOneCheck(cartInfo){
const originIsChecked= cartInfo.isChecked
// 以前是选中的就设置为0,以前不是选中的就设置为1,传递的参数0表示不选中,1表示选中
const isChecked = originIsChecked ? 0 : 1
try{
await this.$store.dispatch('updateCartIscheck',{skuId:cartInfo.skuId,isChecked:isChecked})
alert('修改成功')
// 重新获取购物车请求
this.getShopCartInfo()
}catch(error){
alert(error.message)
}
}
修改购物车的选中状态多个(难点)
- 没有用于全选和全部选按钮的接口,所以这里借用单选的接口来实现
- 要使用的api是
promise.all()
回顾Promise.all()
- 功能:批量处理多个promise对象
- 参数:promise对象的数组
- 返回值:返回一个新的promise对象,只有所有的promise都成功的才成功,只要有一个失败了就直接失败
- 新的promise对象成功的结果:是参数promis对象数组当中每个promise对象成功的结果组成的数组
- 新的promise对象的失败的原因:是参数paromise队形数组中第一个失败的promise对象的失败原因
全选按钮使用v-model
收集了isCheckAll
,当该属性被修改的时候会调用对应的set
方法,所以在计算属性的set方法中完成:
// 计算全选是否选中,这个选中是可以修改的,所以使用完整写法
isCheckAll: {
// 读方法
get() {
return this.cartInfoList.every((item) => {
// 每一项都被选中的时候,则全选为true,只要有一项方法false,则结果为false
return item.isChecked;
});
},
// 通过这个方法进行全选操作
async set(value) {
// 第一个参数就是新的值
try{
// 接收到promise.all()返回的新的promise实例
await this.$store.dispatch('updateCartIscheckAll', value ? 1 : 0)
alert('修改成功')
this.getShopCartInfo()
}catch(error){
console.log(error.message)
}
},
},
对应的Vuex,借用其他请求:
// 修改购物车的选中状态
async updateCartIscheck({commit},{skuId,isChecked}){
const result = await reqUpdateCartIscheck(skuId,isChecked)
if(result.code === 200){
return 'ok'
}else{
return Promise.reject(new Error('checked change failed'))
}
},
// 全选和全部选:这里使用promiseall来实现,采用对那个修改的接口去修改多个,但是真正不是这样的,真正应该使用一个修改多个的接口,但是现在没有
// 没有添加async updateCartIscheckAll返回的结果是Promise.all(promises)返回的新的promise
// 添加了async updateCartIscheckAll返回的是异步函数返回的promise,虽然返回的不是同一个promise但是最终的结果是一样的
// 一位内async函数返回的promise,最终成功或者失败看的是return的Promise.all()返回的promise
async updateCartIscheckAll({getters,dispatch},isChecked){
const promises = []
// 通过getters获取到所有购物车的信息
getters.cartInfo.cartInfoList.forEach((item)=>{
// 如果传递过来的和原来就是一样的,那么就不修改
if(item.isChecked === isChecked) return
// 借用单选的api去发送请求
const promise = dispatch('updateCartIscheck',{skuId:item.skuId,isChecked:isChecked})
promises.push(promise)
})
// 返回结果,如果都成功返回的就是一个promise实例,如果有一个失败就返回失败的promise实例
return Promise.all(promises)
}
删除单个的购物车信息
原理跟修改单选框一样
- 请求接口函数
- Vuex中的异步请求
- 点击触发事件回发送请求进行删除
请求接口函数:
// 删除单个购物车信息/api/cart/deleteCart/{skuId},注意请求方式和参数
export const reqDeleteCart = (skuId)=>{
return request({
url:`/cart/deleteCart/${skuId}`,
method:"delete"
})
}
Vuex中的action:
// 删除单个的购物车信息
async deleteCart({commit},skuId){
const result = await reqDeleteCart(skuId)
if(result.code === 200){
return 'ok'
}else{
return Promise.reject(new Error('delete failed'))
}
}
事件的回调:
// 删除单个的购物车信息
async deleteOne(cartInfo){
try{
await this.$store.dispatch('deleteCart',cartInfo.skuId)
alert('删除成功')
this.getShopCartInfo()
}catch(error){
alert(error.message)
}
}
删除选择中的购物车信息
原理跟全选和全部选一样
action中的代码:
// 删除选中的购物车信息
async deleteCartAll({getters,dispatch}){
const promises = []
getters.cartInfo.cartInfoList.forEach((item)=>{
// 被选中为1,因该要删除,要执行,所以取反,没有白选中的return
if(!item.isChecked) return
const promise = dispatch('deleteCart',item.skuId)
promises.push(promise)
})
return Promise.all(promises)
}
点击删除按钮触发的事件回调:
// 删除被选中的购物车信息
async deleteAll(){
try{
await this.$store.dispatch('deleteCartAll')
alert('删除成功')
this.getShopCartInfo()
}catch(error){
alert(error.message)
}
}