前言
这篇文章总结了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操作:
-
-
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.$router
和this.$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是全局保存的,只有刷新页面数据才会重置.所以有时候可以用来保存组件销毁之前保存的部分信息,而不用重复请求数据.
本文来源: 掘金 如需转载请联系原作者