题图 From Bing By Clm
上一篇文章react结合redux实现了一个购物车功能,本篇给大家演示用vue结合vuex实现相同的购物车功能。
首先看下要实现的页面功能:
观察页面,灰色标签标识了页面的功能,具体功能分析如下:
1、可以实现全选/反选功能,全选/反选功能和每件商品的选中功能联动。
2、商品数量增减功能,商品数量的修改会同步到服务端。
3、商品件数和总价会根据商品选中数量实时计算。
页面结构和功能分析完毕,接下来我们将页面按照UI和功能抽离成组件,因为这个页面我们只关注购物车部分,所以只划分购物车部分的组件,如图:
观察上图,抽离出了五个组件,这张图看着还不是很直观,我们将其转化成一张草图:
组件之间的包含关系如下:
界面、功能、组件都处理好之后,下面就要去处理数据了,我们的数据都放在了由vuex构造的store中,store提供了操作数据的接口,但在使用store之前我们先构造store,用vuex构造store与用redux构造store有些许不同,不过原理都是一样的。
用vuex构造store非常简单,但是要先把数据抽离出来,然后用这个数据结合vuex组装成store,那么我们这个应用需要用到什么数据呢?
首先就是购物车的列表数据,用js表示的话就是一个数组数据,数组每一项应该是一个对象,那么对象中有哪些属性呢?看图说话:
根据上图我们来抽离数据,商品的名称name,价格price,数量count,图片链接img、唯一标识id,标识s是否被选中checked,基本就这么多了。
这里需要注意一下:标识商品是否被选中的属性checked不应该是后端服务器返回给前端数据中包含的属性。这个属性应该是前端应用来维护,前端应用每次刷新页面或者更改是否选中的状态都不应该影响服务器端的数据。关于这一点我们构造store时再做说明。
vuex构造的store的结构如图:
我们可以将store理解成一个智能冰箱,这个冰箱有一些特定的功能可以操作里面的食材,可以通过智能屏幕显示里面的食材还剩多少,可以打开门填充食材,门有上中下三个,分别取不同的食材,我们的数据就是里面的食材,不过要使用这个冰箱,需要我们来根据vuex提供的方法来设计。
我们将所有关于store的文件放到store文件夹中,项目结构如下:
index.js内容如下:
import Vue from "vue";
import Vuex from "vuex";
import car from './modules/car'
Vue.use(Vuex);
export default new Vuex.Store({
modules:{
car
}
});
这里我们为了方便将来的扩展,将购物车store配置项模块抽离出来,放到一个单独的文件中,modules/car。
这样做的话,生成store需要使用vuex的modules配置属性,modules是一个对象类型的配置方式,属性指向对应的模块,这里我们使用了es6的属性简写方式。
再看car模块的代码:
import local from "@/utils/local.js";
let car = {
state: {
carlist: []
},
mutations: {
selectall(state,payload){
console.log(payload)
state.carlist = [...state.carlist.map(e=>{
return {
...e,
checked: payload
}
})]
},
getcarlist(state, payload){
state.carlist = payload
},
updatecar(state, obj){
let index = -1;
state.carlist.forEach((e, i) => {
if(e.id===obj.id){
index=i
}
})
if (index > -1) {
// state.carlist[index] = { ...state.carlist[index], ...obj }
state.carlist.splice(index,1,{ ...state.carlist[index], ...obj })
} else {
state.carlist.push(obj)
console.log(state.carlist)
}
}
},
actions: {
selectall({commit},payload){
commit("selectall",payload)
},
getcarlist(conetext, payload){
let data = local.getdata();
data = data.map(e => {
e.checked = false;
return e
})
conetext.commit('getcarlist', data);
},
frontedupdate(context, payload){
context.commit('updatecar', payload);
},
backupdate(context, payload){
setTimeout(() => {
local.updatecar(payload);
context.commit('updatecar', payload);
});
}
},
getters:{
isall:(state,getters)=>{
return state.carlist.every(e=>{
return e.checked===true;
})
},
allprice:(state)=>{
let arr = state.carlist.filter(e=>e.checked);
let all = 0;
arr.forEach(e=>{
all += (e.count * e.price * 100)
})
return all/100;
},
allcount:(state)=>{
let arr = state.carlist.filter(e => e.checked);
let count = 0;
arr.forEach(e => {
count += e.count
})
return count;
}
}
}
export default car;
阅读代码,我们发现配置一个store的模块需要配置如下几个属性,state、mutations、actions、getters。
我们通过state属性向容器中塞入了一个carlist空数组,通过mutations告诉容器根据相应指令修改carlist,getcatlist指令获取远程数据更新冰箱中的carlist,这里注意一个细节,我们从远端获取数据后并不是直接将数据放进容器,而是用map处理了一下,为每个商品初始化选中状态,初始值false。
updatecar指令根据传递的参数修改carlist,select指令将carlist中商品改为全选或全部选。
在操作store中的state时我们一般不会直接触发mutation,而是通过触发action,然后在action中触发mutation,action内部是可以进行异步操作的,而mutation则不能。
仔细观察action中的getcatlist,我们发现真正从远端获取数据的过程被我们封装到了action中,除了getcarlist这个action需要注意,还有两个action也是需要特别注意的,代码如下:
frontedupdate(context, payload){
context.commit('updatecar', payload);
},
backupdate(context, payload){
setTimeout(() => {
local.updatecar(payload);
context.commit('updatecar', payload);
});
}
frontedupdate与backupdate,仔细观察代码,前者只修改容器中的数据,后者不仅触发mutation修改容器中的数据,还修改了模拟的远端数据。
为什么要这样做呢?前面数据初始化时,获取远端数据,然后为每个商品添加了checked属性,这个属性只能由前端应用来控制,不必和远端同步,而商品其他属性,如数量如果修改需要和远端更新,所有实现了俩个action。
frontedupdate只修改容器中的数据,backupdate不仅要修改容器中的数据,还要修改远端数据。
最后是getters,getters类似容器的一个窗口,通过这个窗口我们能实时观察到数据变化,通过这些变化得到我们想要的数据(被选中的商品的总件数、总价格)。
组件和store都有了,下面就是将store和组件进行聚合了,通俗点说就是将store中的数据渲染到组件中,store中的car模块的carlist数据要在哪里做渲染呢?看图:
carbody组件代码:
在carbody组件中,我们用vuex提供的mapState和mapActions将action和state映射到组件的计算属性和方法上,在created生命周期函数中触发getcarlist的action,store发生变化,和store绑定的dom会自动更新。我们将数组每一项传递到catitem组件中,这里我们应用了es6的扩展运算符方法。
来看一下caritem的代码:
在caritem组件中,我们用props接收父组件传递的参数,并做了约束。
然后直接将接收到的参数渲染到dom中,这里需要注意,有两个props属性我们是需要改变的,checked和count,checked我们只取初始化的值,当触发input的change事件,我们通过触发frontedupdate这个action来修改store,store修改后与其绑定的dom就会更新。
count也是props属性,所以不能直接修改,所以我们直接在组件内部的data属性新建一个newcount属性,初始值为count,我们将newcount绑定到input上,注意不能使用v-model,然后我们通过a标签的点击事件来控制newcount的增减,并且同时触发backupdate这个action,从而更新dom。
count这里也可以使用refs属性,触发a标签的点击事件时,通过refs属性获取input的值,然后进行操作,这样就不用去创建newcount这个数据了。
这里一定要遵循一个原则,store中的数据不能直接修改,修改的话一定要通过触发action来修改。
caritem的功能完成之后,来看看carfooter中的功能,也就是全选与实时结算功能。看一下carfooter组件的代码:
全选/反选的功能分两部分,首先是点击全选复选框能够修改所有商品的选中状态,我们在全选的复选框上绑定一个chang事件,当用户触发这个事件的时候,去触发selectall这这个action,这个action会触发mutation的selectall指令,这个指令会根据传递的第二个参数修改所有商品的选中状态。
另外一个功能就是当我们点击单个商品的选中状态,当所有的商品的选中状态都为true的时候,全选按钮也会变为选中状态,这个如何完成呢?
我们需要一个变量来和这个复选框绑定,并且这个变量是根据所有商品是否都被选中计算出来的,这就需要判断商品是不是都被选中了,所以我们在定义store的时候,在getter中设置了一个叫做isall的属性,看一下这部分的代码:
getters的isall属性是根据所有商品是否被绑定了计算出来的,并且将其暴露出去,然后用mapGetters将其映射到组件的computed的属性上,再和DOM绑定,这样store发生变化,DOM就会实时更新,仔细观察总件数allcount和总价allprice的实现方式,都是一个道理,这里不做多余的演示了。
以上便是用vue结合vuex实现一个购物车的功能,通过上一篇react结合redux的案例来,大家可以总结一下react与vue字使用层面的不同。
源码地址:https://github.com/clm1100/reactcar