从概念到实际项目__vuex指北

  1. 云栖社区>
  2. 前端那些事儿>
  3. 博客>
  4. 正文

从概念到实际项目__vuex指北

我是管理员 2018-07-12 10:21:46 浏览1371
展开阅读全文

前言

这篇文章总结了vuex实际开发涉及到的大部分概念,并加上了很多tips是自己实际开发工程中的经验,最后再加上自己实际项目的vuex构建。总之文字很多,代码也很多,建议大家先收藏(滑稽脸~)再结合官方文档慢慢看。

食用方法: 边看边敲! 建议初学者先看“1,核心概念”,再根据“5,实际项目构建本地”进行本地构建。或者偷懒直接在官方实例上进行验证。如果是在实际开发遇到问题的可以根据目录找到相关的tips,也许就能解决你遇到的问题。总之就是先收藏啦!((^▽^))

1,核心概念

1.1 State: 用于数据的存储,是store中的唯一数据源,类似vue中data对象.

  • 单一状态树:用一个对象就包含了所有应用层级状态.每个应用就只包含一个store实例.
  • 计算属性:由于Vuex的状态储存是响应式的,从store实例中读取状态最简单的方法就是在计算属性中返回某个状态(例如token).
  • 使用方法:

// 定义
new Vuex.Store({
    state: {
        bilibili: {
				acFun:"我还想再活五百年"
			}
    }
    //...
})
// 组件中获取
this.$store.state.bilibili.acFun

Tips:如果某个state是作为公共状态给多个组件使用,且不想被修改后污染其他组件.这时可以将state写成return形式,类似vue中data一样.(仅2.30+以上支持)

    state(){
		return{
			bilibili: {
				acFun:"我还想再活五百年"
			}
		}
    }

1.2 Module: 将store分割成不同的模块,方便管理维护

  • 可以将store分割成模块(module).每个模块拥有自己的state,mutation,action,getter,甚至嵌套子模块--从上至下进行同样的方式的分割.
  • 使用方法:

  • // 定义
    const moduleA = {
        state: { ... },
        mutations: { ... },
        actions: { ... },
        getters: { ... }
    }
    
    const moduleB = {
        state: { ... },
        mutations: { ... },
        actions: { ... }
    }
    
    const store = new Vuex.Store({
        modules: {
            a: moduleA,
            b: moduleB
        }
    })
    
    // 组件中使用
    store.state.a // -> moduleA 的状态
    store.state.b // -> moduleB 的状态
    

    1.3 Getter: 对共享数据进行过滤获取

    • 当需要对 store 中的数据进行处理,或者数据被多个组件复用,就可以使用 Getters 来处理,Getters 也可以理解为 Vue 中的计算属性 (computed),其实就是基于state数据的再包装
    • getter的返回值会根据它的依赖被缓存起来.
    • 使用方法:


    // 定义,第一个参数为该模块下的state;第二个参数getters为store里的getters.注意getters是没有按模块进行区分的;第三个参数rootState顾名思义就是根state
    getters: {
        cartProducts(state, getters, rootState) 
            => (getters.allProducts.filter(p => p.quantity)),
    
    	dateFormat(state, getters) {
                let date = state.nowDate;
                return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} / ${date.getHours()}:${date.getMinutes()}`;
            }
    }
    // 组件中获取
    this.$store.getters.cartProducts
    
    //补充:由于getters会缓存结果.如果你不想缓存,或者想对getters进行传参,此时则需要用函数形式写getter.
    getters:{
    //...
    	test:(state)=>(param)=>{
    		return state.todos.find(todo=>todo.id===id)
    	}
    }
    
    

    1.4 Mutation: 改变state的唯一方法

    • 每个mutation都有一个字符串的事件类型(type) 和一个 回调函数
    • mutation必须是同步函数
    • mutation不能包含异步操作
    • 由于store中状态是响应式的,那么在修改时,mutation也应该满足vue响应式的一些注意事项:
      • 最好提前在你的 store 中初始化好所有所需属性。
      • 当需要在对象上添加新属性时,你应该
        • 使用 Vue.set(obj, ‘newProp’, 123),或者
        • 以新对象替换老对象.例如,利用 stage-3 的对象展开运算符我们可以这样写:
    	state.obj = { ...state.obj, newProp: 123 }
    复制代码
    • mutation是修改state的唯一方法,并且mutations不能直接调用,而要通过相应type调用store.commit.
    • 使用方法:


    // 定义 第一个参数state为该模块下的state;第二个参数products为调用时的传参
    mutations: {
        setProducts (state, products) {
            state.allProducts = products
        }
    }
    
    // 组件中提交方式可以分为三种,其实前两种都是载荷(payload),第三种是对象形式
    
    //第一种 直接在后面加上要传入的参数
    this.$store.commit('setProducts', 'GodOfWar4')
    
    //第二种 直接在后面加上要传入的参数
    this.$store.commit('setProducts', {
    	name:'GodOfWar4',
    	comment:"我TM射爆!"
    	})
    //注意:此时mutation,setProducts 就要修改为下面形式
    setProducts (state, products) {
            state.allProducts = products.name //要改为这种写法
        }
    
    //第三种 将state类别作为对象的属性,和参数一起提交
    this.$store.commit({
    	type:'setProducts',
    	name:'GodOfWar4',
    	comment:"我TM射爆!"
    	})
    //此时mutation的写法和第二种情况一样.
    
    

    1.5 Action: 可以使用异步操作提交mutation

    • action提交的是mutation,而不是直接变更状态.action可以包含异步操作
    • action通过store.dispatch触发(异步)
    • action返回的是promise
    • 如果是state的数据就使用actions请求数据,建议数据处理也放在actions中,或者放在getter中进行数据处理.mutations只做state的修改.
    • 使用方法:


          state: {
             count: 0
                 },
          mutations: {                
             increment (state) {
              state.count++
             }
              },
          actions: {         //只是提交`commit`了`mutations`里面的方法。
             increment (context,payload) {
              context.commit('increment')
       		}
     	 }
    
    
     // 一般我们会通过解构简写成这样
      actions: {
       increment ({ commit },payload) {
             commit('increment')
          }
             }
    
    // 在组件中使用,同mutation,只是由commit变为dispatch
    this.$store.dispatch('increment', {//..payload})
    
    
    //这里需要说明的是第一个参数context就是上下文,context是一个store对象,你也可以用解构的方式写出来,第二个参数payload是我们的传参,同mutation一样.
    //那么context究竟包含了哪些?通过源码可以清楚看到:
    
    let res = handler({ 
       dispatch,
       commit,
       getters: store.getters,
       state: getNestedState(store.state, path),
       rootState: store.state
      }, payload, cb)
    
    //可以看出context包括5个属性:dispatch,commit,getters,state,rootState.
    //故我们可以通过解构的方式{commit,dispatch,state}只取我们需要的属性.
    

    • 组合 Action (完全照搬官网说明)

    Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

    首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch仍旧返回 Promise



    actions: {
      actionA ({ commit }) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            commit('someMutation')
            resolve()
          }, 1000)
        })
      }
    }
    
    

    现在你可以:

  • store.dispatch('actionA').then(() => {
      // ...
    })
    
    

    在另外一个 action 中也可以:

  • actions: {
      // ...
      actionB ({ dispatch, commit }) {
        return dispatch('actionA').then(() => {
          commit('someOtherMutation')
        })
      }
    }
    
    

    最后,如果我们利用 async / await,我们可以如下组合 action:

  • // 假设 getData() 和 getOtherData() 返回的是 Promise
    
    actions: {
      async actionA ({ commit }) {
        commit('gotData', await getData())
      },
      async actionB ({ dispatch, commit }) {
        await dispatch('actionA') // 等待 actionA 完成
        commit('gotOtherData', await getOtherData())
      }
    }
    
    

    一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
  • Tips:

    • 有一点要注意的是,将 store 中的 state 绑定到 Vue 组件中的 computed 计算属性后,对 state 进行更改需要通过 mutation 或者 action,在 Vue 组件中直接进行赋值 (this.myState = ‘ABC’) 是不会生效的。

    • 在 Vuex 模块化中,state 是唯一会根据组合时模块的别名来添加层级的,后面的 getters、mutations 以及 actions 都是直接合并在 store 下。

    • 由于vuex是单向数据流,vue中v-model是双向绑定.所以当v-model绑定的数据时vuex时,需要监听实时修改vuex中的数据.

    2,图例分析

    • 文件夹结构:

    • 2a25c20adb9e35b56bc8322a0cf464e3010d466f

    • vuex组织结构:

    • ac1ef1edddf6d292d0ee236c9409b66f38d3194c

    • vuex操作:

    • 9c76d1aff71aba1108d14834bb01506f79419dba

    • 3,Vuex安装

      此部分参考了# Vue组件通信深入Vuex,在此表示感谢!

      • 3.1 在项目中安装Vuex:

      • npm install vuex --save
        


      • 3.2 在src目录下新建store/index.js,其中代码如下:

      • import Vue from 'vue'
        import Vuex from 'vuex'
        // 修改state时在console打印,便于调试
        import createLogger from 'vuex/dist/logger'
        
        Vue.use(Vuex)
        
        const debug = process.env.NODE_ENV !== 'production'
        
        const state = {}
        const getters = {}
        const mutataions = {}
        const actions = {}
        
        export default new Vuex.Store({
            state,
            getters,
            mutataions,
            actions,
            // 严格模式,非法修改state时报错
            strict: debug,
            plugins: debug ? [createLogger()] : []
        })
        


      • 3.3 在入口文件main.js中添加:

      • // ...
        import router from './router'
        import store from './store'
        
        new Vue({
            el: '#app',
            router,
            store,
            // ...
        })
        


      • 可以对比vue-router和vuex的安装方式:它们均为vue插件,并在实例化组件时引入,在该实例下的所有组件均可由this.$routerthis.$store的方式查询到对应的插件实例
      • 4,辅助函数用法

        • 再次强调在 Vuex 模块化中(即使用module写法),state 是唯一会根据组合时模块的别名来添加层级的,后面的 getters、mutations 以及 actions 都是直接合并在 store 下。所以辅助函数也是同样符合上述规定

        • 注意: 这里为了简便,都使用了...拓展运算符.并且我们 所有使用的辅助函数都是写在device模块(module)内!

        4.1 mapState

        写在computed的情况



        // 首先引入mapState
        import {mapState} from 'vuex'
        
        export default {
        //1,写在computed的情况
          computed: {
         ...mapState({
          //这里需要注意的是,由于我们是使用module的,所以需要写成加上device,并且是这种箭头函数的形式.
          test1: state => state.device.test
        
        //test1:'test'  这种写成字符串形式,等价于state=>state.test
        //test1(state){
        //	return state.device.test + this.message
        //} 如果返回值中需要用到组件this,则需要写成这种函数形式
        
          }), 
          },
        }
        
        //这种情况可以直接通过this.test1来得到state.device.test
        
        


      • 写在methods的情况

        这中写法最大的不同就是,务必要写成this.test1()才能得到state.device.test


// 首先引入mapState
import {mapState} from 'vuex'

export default {
//2,写在methods的情况,写法和在computed中基本是一样的.但注意的是:mapState是不能直接写在某个函数体内的!只能像这样写在methods内.
  methods: {
 ...mapState({
  //这里一样也是支持三种写法的,看实际情况选择
  test1: state => state.device.test

//test1:'test'  
//test1(state){
//	return state.device.test + this.message
//} 

  }), 
  },
}

//这中写法最大的不同就是,务必要写成this.test1()才能得到state.device.test直接写成this.test得到的只是获取返回state的函数


注意: 常用的做法是将state中数据使用getter包装后输出,因此,mapState在项目中较少遇到. 其实,个人感觉这两种情况的写法都不是太好,都比较麻烦.个人建议还是直接使用let test1=this.$store.state.device.test这种写法最简单直接明了,复杂的数据就使用getters.

4.2 mapGetters

写在computed的情况


// 首先引入mapGetters
import {mapGetters} from 'vuex'

export default {
//1,写在computed的情况
  computed: {
 ...mapGetters({
  //这里需要注意的是,与mapState不同的是,我们虽然使用了module,但并不能加上device,这里vuex是把所有的getters合在了一起,里面并没有device进行模块划分.所以只能写成字符串形式.并且注意:如果device里的getters属性名与根getters的属性名一样.根getters的属性名则会就进行覆盖.
  test1: 'test'


  }), 
  },
}

//同样这种情况可以直接通过this.test1来得到getters.test



写在methods的情况

这中写法最大的不同就是,务必要写成this.test1()才能得到state.device.test


// 首先引入mapGetters
import {mapGetters} from 'vuex'

export default {
//2,写在methods的情况,写法和在computed中基本是一样的.但注意的是:mapGetters是不能直接写在某个函数体内的!只能像这样写在methods内.
  methods: {
 ...mapGetters({
  //这里一样也是只支持字符串写法的
  test1:'test'
  }), 
  },
}

//这中写法最大的不同就是,务必要写成this.test1()才能得到getters.test



再次重申:与mapState不同的是,我们虽然使用了module,但并不能加上device,这里vuex是把所有的getters合在了一起,里面并没有device进行模块划分.所以只能写成字符串形式.并且注意:如果device里的getters属性名与根getters的属性名一样.根getters的属性名则会就进行覆盖.并且写法只能是{test1:'test'}或者['test']此时this.test就等于getters.test

4.3 mapMutations

mapMutations映射的是store.commit('mutation名',传参)而不是mutation函数,并且和mapGetters一样,是把所有的mutation合在了一起,所以无法通过模块名进行区分,只能自己在命名时进行区分.或者使用自带的命名空间.



// 首先引入mapMutations
import {mapMutations} from 'vuex'

export default {
//此时需要写在methods上,写法和mapGetters基本是一样的.可以写成对象形式和数组形式
  methods: {
 ...mapMutations({
  test1:'test'
  }), 
// ...mapMutations([
//  'test'  //此时this.test(param)映射为this.$store.commit('test',param)
//  ]), 
//  },
}




4.4 mapActions

mapActions映射的是store.dispatch('action名',传参)而不是action函数,并且和mapGetters一样,是把所有的action合在了一起,所以无法通过模块名进行区分,只能自己在命名时进行区分.或者使用自带的命名空间.



// 首先引入mapActions
import {mapActions} from 'vuex'

export default {
//此时需要写在methods上,写法和mapGetters基本是一样的.可以写成对象形式和数组形式
  methods: {
 ...mapActions({
  test1:'test'
  }), 
// ...mapActions([
//  'test'  //此时this.test(param)映射为this.$store.dispatch('test',param)
//  ]), 
//  },
}



5,实际项目构建

5.1,文件结构构建

首先声明下,vuex实际项目构建有很多人是以功能进行划分模块.例如:


store
    ├── index.js             # 导出 store 的地方
    ├── state.js             # 根级别的 state
    ├── getters.js           # 二次包装state数据
    ├── actions.js           # 根级别的 action
    ├── mutations.js         # 根级别的 mutation

但我更倾向于以业务逻辑进行划分模块,毕竟我们构建项目的src和构建vue也都是以业务逻辑进行区分的.所以实际项目使用的是以modules进行模块划分的.具体可以参考上图的vue结构组织形式.文件夹结构如下:(下面我也会以这种结构进行讲解)


store
    ├── index.js             # 导出 store 的地方
    ├── modules              # modules文件夹
    	├── home.js          # home 的模块文件
    	├── device.js        # device 的模块文件
    	├── event.js         # event 的模块文件
        ├── order.js         # order 的模块文件
    	├── user.js          # user 的模块文件
    	├── log.js           # log 的模块文件
        ...


5.2,index.js文件配置


import Vue from 'vue'  //引入vue
import Vuex from 'vuex' //引入vuex
//引入你的module模块
import home from './modules/home'
import device from './modules/device'
import event from './modules/event'
import order from './modules/order'
import user from './modules/user'
import log from './modules/log'

Vue.use(Vuex)
export default new Vuex.Store({
//这里可以存放和处理一些公共的信息,例如token,用户信息,初始化信息等.
  state: {
    token: '123',
    userInfo: {
      userId: 0,
      userName: '',
      roleId: 0,
      roleName: ''
  	},
    initialInfo: {
      menuList: [],
      functionList: [],
      projectNodeList: []
    } 
  },
  mutations: {
    setToken (state, token) {
      state.token = token
  	},
    setUserInfo (state, userInfo) {
      state.userInfo = userInfo
  	} 
  },
 //这里填写我们引入的module模块
  modules: {
	home,
    device,
	event,
	order,
	user,
	log
  }
})


5.3, module模块组件的编写.

这里以device模块为例进行讲解:


import Vue from 'vue';

const device = {
  namespaced:true,  //这里我使用了命名空间

state: {
//这里面的state结构就与你device中vue组件的结构进行对应
  //例如:leftTree对应的是device文件下的leftTree.vue组件
  leftTree:{
	  //我一般会将组件需要存放的数据分成这三类进行分别存放,当然你也可以根据自己的需求,自行配置或者不要
	  output:{ 
		//暴露出来的公共数据
	  },
	  update:{
	      //是否更新组件的数据
	  },
	  cache:{
		//组件缓存数据
	  } 
  },
},

getters: {
  leftTree_checkedNode: state=>{
    return state.leftTree.output.checkedNode
  },
  leftTree_update_tree:state=>{
    return state.leftTree.update.tree
  }
},

mutations: {
    leftTree_checkedNode(state,val){
      state.leftTree.output.checkedNode=val
  },
    leftTree_update_tree(state,val){
      state.leftTree.update.tree=val
  },
},

actions: {

  }
};

export default device;



5.4, 在vue组件中的调用.


//注意这里的写法都是按module分模块并加上命名空间的写法
this.$store.state.device.leftTree.output.checkedNode  //这样写会很长
this.$store.getters['device/leftTree_checkedNode']    //这样写会简短点,如果没有命名空间,则不写device/

this.$store.commit('device/leftTree_update_tree',true) //执行device里的mutation方法.同样,没有命名空间,则不写device/


最后我还是想说一下:其实当你想给state设置一个复杂的对象,但mutation里却没有实现方法时,你是可以使用vue.set方法来实现的.也就是说vue.set其实也是可以修改state状态的.但一般建议还是使用mutation.方便管理维护.

6, 使用场景

最后简单提一下vuex的使用场景:

  • 两个以上组件共享的数据.
  • 多个兄弟组件共享的数据.
  • 方便兄弟组件间修改共享的数据.
  • 全局共享数据,如:token.
  • 核心业务数据.

一些实际应用

  • 由于vuex是全局保存的,只有刷新页面数据才会重置.所以有时候可以用来保存组件销毁之前保存的部分信息,而不用重复请求数据.
原文发布时间为:208年06月25日
原文作者:suchName
本文来源:掘金  如需转载请联系原作者

网友评论

登录后评论
0/500
评论
我是管理员
+ 关注
所属团队号: 前端那些事儿