从概念到实际项目__vuex指北

2,930 阅读6分钟

前言

这篇文章总结了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,图例分析

  • 文件夹结构:

    文件夹结构

  • vuex组织结构:

    vuex组织结构

  • vuex操作:

    vuex操作

  • vuex的数据流向:

    vuex的数据流向

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是全局保存的,只有刷新页面数据才会重置.所以有时候可以用来保存组件销毁之前保存的部分信息,而不用重复请求数据.

最后

如果大家发现了哪里有不对,或者异议的地方,欢迎留言拍砖!谢谢!