zoukankan      html  css  js  c++  java
  • vue.js移动端app实战3:从一个购物车入门vuex

    什么是vuex?

    官方的解释是:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
    

    简单来说就是集中管理所有的状态

    为什么要用vuex?

    • 对于父子组件之前的通信,父组件通过porps传递到子组件,子组件通过$emit发送事件都到父组件;

    • 对于组件与组件之间的通信,可以new一个新的Vue实例,专门用来做event bus进行通信。

    当多个组件之间共享一个状态时,event bus可能就变成这样。

    而使用vuex,可以变成这样。

    回到我们的项目,需要共享状态的总共有3组件:

    这三个组件都需要用到购物车列表goodsList

    • 对于详情页面,需要判断当前点击的电影是否已加入购物车,如果goodsList中已存在,则不再加入;
    • 对于底部导航,当goodsList数量>0时需要显示数字。
    • 购物车组件就不用说了,基本所有的状态都需要。

    如何使用

    首先安装:cnpm install vuex --save
    

    安装好后,新建一个store文件同时新建index.js文件,引用并且使用vuex

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex);
    

    其次,导出一个vuex.Store实例,可接受一个对象作为参数:

    {
        state,      <!--状态-->
        
        getters,    <!-- 状态的计算属性 -->
        
        mutations,  <!-- 用于改变状态 -->
        
        actions,   <!-- 可包含异步操作的mutation -->
    }
    

    我们先只传入state

    export const store= new Vuex.Store({
        state:{
            goodsList:[]
        }
    })
    

    接着,在main.js中引入并挂载到Vue实例上

    ...
    
    import {store} from './store/index.js'
    
    new Vue({
      el: '#app',
      router,
      store,
      render: h => h(App)
    })
    

    在购物车 car.vue组件通过一个计算属性获取vuex的状态goodsList

     computed:{    
             goodsList(){
                return this.$store.state.goodsList
            }
        }
    

    这样我们就可以通过v-for将购物车列表循环出来了,不过现在是数组空的,我把添加的按钮放到电影详情里面去了。

    我们在首页的电影列表套了一层router-link,并将电影的id作为参数,所以点击时就会入到详情页面。
    
    <router-link tag="li" v-for="(v,i) in array" :key="v.id" :to='{path:"/film-detail/"+v.id}'>  
    

    在详情页面js中,我们通过this.$route.params.id获取(参数key为id取自我们路由的配置)。

    获取到id后,接下来就是在某个生命周期(通常是mounted)发送请求获取该电影的data,然后就是赋值操作让数据显示出来了。这里主要讲一下 activated生命周期,由于我们在App.vue使用了keep-alive,所以film-detail组件在第一次进入后就会被缓存,由于该组件不会被销毁,所以之后我们每次进来都会保持第一次进来获取的数据。

    因此,我们将发送请求的时间点由之前的mounted(已挂载)改变为activated(组件激活时),这样既能复用组件,又能保证每次进入时都获取最新的数据。

    回到vuex,点击详情里面的按钮时,要将该电影加入到购物车,也就是说要改变state的状态。
    

    vuex规定改变store中的状态的唯一方法是提交mutation,虽然你也可以直接改变,比如点击某个按钮时 this.$store.state.number++,不过最好时通过mutation触发。通常我们走路都是正着走的,假如你非要倒立着走,也没办法拦着你。

    定义mutation

    mutations:{
        add:state=>state.number++                     
    }
    

    使用mutation

    <!-- 在某个组件里面使用mutation -->
    this.$store.commit("add");
    
    为了将电影加入购物车,在导出的实例的参数中添加 mutations
    
    export const store= new Vuex.Store({
        state:{
            goodsList:[]
        },
        mutations:{
            addGoods:(state,data)=>{
                state.goodsList.push(data);                      
            },
        }
    })
    
    点击按钮时,首先判断该电影是否在购物车已存在,如果存在则不再加入。
    
    var idExist=this.$store.state.goodsList.find((item)=>{
        return item.id==id
    })
    

    使用es6数组新增find函数来实现,该函数返回满足条件的第一个元素。如果不存在该id,则将对应的数据存入。

        if(!idExist){
            var data={
               url:this.smallPic,
               title:this.title,
               price:Math.floor(Math.random()*100),
               stock:"盒",
               number:1,
               select:false,
               id:this.id
            }
            this.$store.commit("addGoods",data);
            this.addSuccess=true;           
    
        }else{
            return alert("已加入購物車")
        }
    
    为了给加入购物车成功一个反馈,写个简单的效果,让+1缓缓移动并且透明度慢慢消失
    
    <span class="add-to-car__tip show" v-if="addSuccess">+1</span>
    
    <!-- 核心css -->
    
    span{
     animation:move 1.6s forwards;  
    }
    
    @keyframes move{
           from{
               opacity: 1;
               transform:translateY(0);
    
           }
    
           to{
               opacity: 0;
               transform:translateY(-100%);
           }
       }
    

    详情页面搞定后,来看底部导航 。

    当购物车数量大于0时,底部导航需要显示当前购物车的数量,也即是goodsList.length;
    

    可以通过this.$store.state.goodsList.length来取得,不过最好的办法是通过vuex的getters来获取。因为假如你有多个页面需要用到这个length时,你可能就会在每个需要用到的地方复制这段代码过来“this.$store.state.goodsList.length”。

    在配置参数中加一个getters
    
    export const store= new Vuex.Store({
        state:{
            goodsList:[]
        },
        getters:{
            goddsNumber:state=>{
                return state.goodsList.length
            }
        },
        mutations:{
            addGoods:(state,data)=>{
                state.goodsList.push(data);                      
            },
        }
    })
    
    使用的方法跟获取state基本一致,只不过是由state.xx改为getters.xx
    
    computed:{
           number(){
              return this.$store.getters.number
          }
      }
    
    我们希望当number>0才显示,无非就是一个v-show="number>0"
    

    接着是购物车页面。

    购物车涉及到的操作有:数量加,数量减,是否选中,删除,
    看起来需要4个方法,实际上总结一下2个方法就够了:

    • 1个是delete

    • 1个是update

    delete时:获取index后通过splice(index,1);
    update时:同样的需要获取index,之后只需要判断哪个状态需要改变进行操作即可。比如number+1,或者number-1,或者选中状态为true变为false,由false变为true。我们只需要将要改变和key和要设置的value都作为参数,就可以实现1个方法进行多个操作。
    在mutation中再加2个方法:
    
    mutations:{
    
       deleteGoods(state,index){
           state.goodsList.splice(index,1);    
       },
       
       updateGoods(state,data){
          <!--index为操作第几个元素,key为要改变的key,value为新的值 -->
          
           const {index,key,value}=data;
           state.goodsList[index][key]=value;  
       }
    }
    
    2个方法都需要知道index即需要操作哪一个元素。虽然在购物车这个数组中,我们在循环时已经知道index了,但这不一定就是你需要的那个,除非购物车不分页,一次展示所有数据。假如购物车有100个商品,而且进行了分页,每次取20条数据,那么你点击列表的第一个元素,分页后则会分别对应数组的0,20,40,。。。180,而不分页的第一个元素的index永远是0。因此,我们需要获取元素真正的位置。每个电影都有自己唯一的ID,通过es6数组的findIndex并传入对应的ID,可以返回元素的位置。

    写一个方法:

    findPosition(id){
          return this.goodsList.findIndex(item=>{
              return item.id==id
           })
     },
    

    点击删除时:

     del(id){
          var i=this.findPosition(id);
          this.$store.commit("deleteGoods",i);
      },
    

    点击切换选中时:

    toggleSelect(id){
         var i=this.findPosition(id);
         var select=this.goodsList[i].select;
         this.$store.commit("updateGoods",{
              index:i,
              key:"select",
              value:!select
         });          
    }
    

    点击加减号时,传入+1或者-1:

     changeNumber(id,val){
          var i=this.findPosition(id);
          var number=this.goodsList[i].number;
            this.$store.commit("updateGoods",{
                index:i,
                key:"number",
                value:number+val<=0?1:number+val
           })
      }
    
    vuex提供了mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用,当有多个mutation需要使用时,使用mapMutations可以让代码更为简洁。
    
    import { mapMutations } from 'vuex'
    
    //在methos中使用展开运算符混入到原有方法中,比如:
    
      methods:{
          ...mapMutations(
              ["deleteGoods","updateGoods"](向methods混入2个方法)
          ),
    
        changeNumber(){
            ...(原有1个方法)
        }
    }
    
    混入后,现在就有3个方法了,可以通过this.deleteGoods和this.updateGoods调用。
    

    假如你不想使用原有的名字,想起一个更酷的名字,可以这么写

    ...mapMutations({
          coolDelete: 'deleteGoods',
          coolUpdate,'updateGoods'
    })
    
    这样一来,点击删除时:(更新的也同理)
    
    del(id){
        var i=this.findPosition(id);
        this.coolDelete(i);
    }
    
    除了mutaion有mapMutation外,state,getters和actions也都有辅助的map函数,可以使用Mutation,可以一次获取多个状态和方法。
    

    至此,基本上已经实现了用vuex进行购物车的增删改。不过每次刷新后,购物车的数据都被清空了。可以配合Localstorage保存到本地。 实现也很简单,每次mutation操作后将state中的goodsList存入到localstorage即可。每次启动服务后,判断localstorage是否有值,有值得话用json.parse转化为数组赋值给state.goodList,没有值得话则为state.goodsList设置默认值为空数组[ ];

    完整文件如下:store.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex);
    
    export const store= new Vuex.Store({
        state:{
    		goodsList:localStorage["goodsList"]?JSON.parse(localStorage["goodsList"]): []  
    	},
    	getters:{
    		sum:state=>{
    			var total=0;
                state.goodsList.forEach((item)=>{
                    if(item.select){
                        total+=item.price*item.number
                    }             
                })
                return total
    		},
    		goddsNumber:state=>{
    			return state.goodsList.length
    		}
    	},
    	mutations:{
    		addGoods:(state,data)=>{
    			state.goodsList.push(data);
    			localStorage.setItem("goodsList",JSON.stringify(state.goodsList));						
    		},
    		deleteGoods(state,index){
    			state.goodsList.splice(index,1);		
    			localStorage.setItem("goodsList",JSON.stringify(state.goodsList));
    		},
    		updateGoods(state,data){
    			const {index,key,value}=data;
    			state.goodsList[index][key]=value;	
    			localStorage.setItem("goodsList",JSON.stringify(state.goodsList));
    		}
    	}
    })
    

    car.vue

    <template>
       <div class="car-list-container">
            <ul>
    	    	<li class="car-list" v-for="(v,i) in goodsList">
    	    		<div class="car-list__img">
    	    			<img :src="v.url">
    	    		</div>
    	    		<div class="car-list__detail">
    	    			<p class="car-list__detail__title">{{v.title}}</p>
    	    			<p class="car-list__detail__number">数量:<button class="number--decrease iconfont icon-jianhao" @click="changeNumber(v.id,-1)"></button><input type="text" readonly="" v-model="v.number"><button class="number--increase iconfont icon-iconfont7" @click="changeNumber(v.id,1)"></button></p>
    	    			<p class="car-list__detail__type">规格:<span>{{v.stock}}</span></p>
    	    			<p class="car-list__detail__price">单价:<span>¥{{v.price}}</span></p>
    	    			<p class="car-list__detail__sum">小计:<span>¥{{v.price*v.number}}</span></p>
    	    		</div>
    	    		<div class="car-list__operate">
    	    			<span class="iconfont icon-shanchu delete-goods" @click="del(v.id)"></span>
    	    			<label >
    	    				<input type="checkbox" name="goods" :checked="v.select==true" @change="toggleSelect(v.id)">
    	    				<span></span>
    	    			</label>
    	    		</div>	   	    		
    	    	</li>
    	    </ul>
    	    <div class="car-foot-nav">
    	    	<button class="sum-price">总额:¥{{sum}}</button>
    	    	<router-link :to='{name:"index"}' class="continue-shopping" tag="button">继续购物</router-link>
    	    	<button class="to-pay">去结算</button>
    	    </div>
       </div>
    </template>
    
    <script>
    import { mapMutations } from 'vuex'
    import { mapGetters } from 'vuex'
    export default {
        name: 'car',
        data () {
            return {      
               
            }
        },
       
        methods:{
            ...mapMutations(
                ["deleteGoods","updateGoods"]
            ),
            findPosition(id){
                return this.goodsList.findIndex(item=>{
                    return item.id==id
                 })
            },
    
            changeNumber(id,val){
                var i=this.findPosition(id);
                var number=this.goodsList[i].number;
                this.updateGoods({
                      index:i,
                      key:"number",
                      value:number+val<=0?1:number+val
                 })
            },
    
            del(id){
                 var i=this.findPosition(id);
                this.deleteGoods(i);
            },
    
            toggleSelect(id){
                var i=this.findPosition(id);
                 var select=this.goodsList[i].select;
                 this.updateGoods({
                      index:i,
                      key:"select",
                      value:!select
                 })            
            }
        },
        computed:{
            ...mapGetters(
                [ "sum"]
            ),   
             goodsList(){
                return this.$store.state.goodsList
            }
        },
        mounted(){
          this.$ajax.get("/api/car",function(res){
            console.log(res)
          })
        }
    };
    </script>
    

    代码已上传到github,点击查看

    项目地址:https://github.com/linrunzheng/vueApp

  • 相关阅读:
    Lesson_strange_words6
    Lesson_strange_words3
    Lesson_strange_words4
    Lesson_strange_words1
    Lesson_strange_words2
    关于我们子页面
    关于我们页面
    走进龙门石窟子页面
    3.用户登陆注册
    2.项目初始化
  • 原文地址:https://www.cnblogs.com/zhengrunlin/p/7446180.html
Copyright © 2011-2022 走看看