在之前组件间通信的时候提到过,可以用一个类似中介一样的东西收集和保存所有状态,让所有的组件通用。如果在Vue之外定义一个对象,然后在Vue中引用对象,这个对象也会变成响应式的。
在简单的应用中,一般就使用简单的一个对象集中存储一下数据即可,比如一个全局变量。但是在复杂的应用里,就需要用一个东西来集中保存状态。其实可以把Vuex认为是一个数据库,可以向其中写入数据,获取数据(自动的而且是响应式的),Vuex帮你把状态和改变都记录下来。
Vuex和Vue Router一样都是由官方维护的库,文档地址。
安装Vuex和导入
Vuex和Vue Router一样,都是需要包含在最终发行版里的,所以可以无需安装-dev,直接安装普通依赖即可。和VueRouter很类似,也是Vue的一个插件,需要先安装再启用:
- 安装
Vue-router
本身:npm install vuex --save
- 在
main.js
中启用路由插件:import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex)
官方在这里还有一个小提示,就是这个库依赖于Promise,如果浏览器不支持Promise就需要引入一个库。
可以通过CDN引入比较方便:<script src="https://cdn.bootcss.com/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
,不过既然前端工程化,还是使用NPM安装和导入吧。
初步配置Vuex和使用数据
既然是一个数据库,就要使用Vuex通过一个对象新创建一个实例,其中可以传入一个option对象。这个Vuex实例就作为存储的库,被称为store
。
//配置Vuex const store = new Vuex.Store({ state:{ count: 0 } });
这个store的数据都保存在state属性中。
然后需要在根Vue实例中将其引入:
const app = new Vue({
router,
//导入store属性
store,
render: h => h(App)
}).$mount("#app")
在根实例中导入之后,只要是作为根实例内部的所有组件,都可以通过this.$store.state
访问到store
对象,其中可以直接使用具体的数据作为名称获取数据,比如this.$store.state.count
。
store
对象,在根实例内部的所有组件中,访问到的都是同一个对象,有点像Vue Router
中的$router
路由器对象。
直接访问全局store
变量一般不推荐,因为会造成高耦合,一般的做法是使用一个计算属性。在组件内部,this.$store.state.count
是只读的,即无法通过这个来设置store中的值。
<template>
<div>
<p>首页内容</p>
<router-view></router-view>
<p>Count的数值是: {{count}}</p>
</div>
</template>
<script>
export default {
name: "about",
computed:{
count: function () {
return this.$store.state.count;
}
}
}
</script>
<style scoped>
div {
font-size: 3rem;
text-align: center;
}
</style>
可以看到,这样就读出来了其中的数据。
如果要修改数据怎么办。需要在store对象中配置mutations
选项,所有的提交都要通过这个选项中的方法来操作。
Vuex对象
Vuex对象的选项有如下几种:
state
state选项中保存了所有的数据。在刚才的例子中已经使用过了。
既然一个Vuex对象保存了状态或者说是数据,不外乎两种方式,一种是存,一种是取。
取的方法已经说过了,通过在根Vue实例中注入store属性,所有的组件都可以访问到Vuex对象来获取具体的数据。
官方文档在这里还提供了一些简化的手段,方便导入很多状态数据,可以了解一下。
mutation
mutation
选项中,所有的方法都是用来修改state
数据中,外界想要修改状态,必须通过提交mutation
中的方法名称来进行操作,无法直接操作。
接着上边的例子,给count
定义一个加1和减1的mutation
方法:
const store = new Vuex.Store({ state:{ count: 0 }, mutations:{ increase: function (state) { state.count++; }, decrease: function (state) { state.count--; } } });
方法已经定义好了,现在看看如何通过组件来修改状态,添加两个按钮和对应的事件:
<template> <div> <p>首页内容</p> <router-view></router-view> <p>Count的数值是: {{count}}</p> <p> <button @click="increase">+1</button> <button @click="decrease">-1</button> </p> </div> </template> <script> export default { name: "about", computed:{ count: function () { return this.$store.state.count; } }, methods:{ increase: function () { this.$store.commit("increase"); }, decrease: function () { this.$store.commit("decrease"); } } } </script>
mutation方法还可以接受第二个参数,用于进行操作:
mutations:{ increase: function (state, 3) { state.count = state.count + 3; } }
第二个参数还可以是一个对象,包含了传入进来的所有参数,这样使用就很灵活:
mutations:{ increase: function (state, params) { state.count = state.count + params.count; } } //commit的时候不使用方法名,而使用一个对象,其中的type=方法名,count=传给params的属性名称和值 this.$store.commit ({ type :'increase', count : 10 }) .
推荐使用第二种方式,比较灵活。最好通过函数名的语义区分不同的方法,比如increaseBy3
,increaseByCount
等。
在mutations
里尽量不要异步修改数据,否则不知道何时会更新数据。
getter
Java里看到getter
基本上就知道是怎么回事了。这里的getter
也是这个意思,在取出数据的时候进行计算并缓存,就像是针对store
的计算属性一样。
本小节里提到getter
指的是这一类方法或者功能;提到getters
指的是Vuex实例的getters
属性。
如果很多组件都需要基于原始数据进行一些逻辑,比如上边例子里,一些组件希望获取count的5倍结果,一些组件希望获取count的10倍结果,与其把逻辑在每个组件的计算属性里写一遍,可以考虑统一写在Vuex的getters
属性中。
getters:{ count5:function (state) { return state.count * 5; }, count10: function (state) { return state.count * 10; } }
在取数据的时候,就需要通过this.$store.getters
加上属性名来取值,修改index.vue
:
<template> <div> <p>首页内容</p> <router-view></router-view> <p>Count的数值是: {{count}}</p> <p>5倍Count的数值是: {{count5}}</p> <p>10倍Count的数值是: {{count10}}</p> <p> <button @click="increase">+1</button> <button @click="decrease">-1</button> </p> </div> </template> <script> export default { name: "about", computed:{ count: function () { return this.$store.state.count; }, count5: function () { return this.$store.getters.count5; }, count10: function () { return this.$store.getters.count10; }, }, methods:{ increase: function () { this.$store.commit("increase"); }, decrease: function () { this.$store.commit("decrease"); } } } </script>
getters
中的方法可以接受第二个参数,第二个参数指向vuex
实例的getters
属性,这样就可以在一个getter
方法内调用其他的getter
方法。
比如count
乘以5再加3,可以复用count5
:
count5plus3: function (state, getters) { return getters.count5 + 3; }
当然,这个例子的业务逻辑过于简单,无需特意复用,可以直接计算。接受第二个参数的好处是可以使用其他getter
方法像搭积木一样构建更复杂的业务逻辑。
还可以让getter
方法返回一个函数,用于传递参数来取值,比如返回一个方法,参数是倍数:
countByParam:function (state) { return function (multi) { return state.count * multi; }; }
在index.vue
的计算属性中可以传递参数:
countByParam:function (state) { return function (multi) { return state.count * multi; }; }
这里返回一个函数,可以对其传入参数,计算指定的倍数,在index.vue
中可以动态的传入参数:
<template> <div> <p>根据参数计算的结果是: {{countByParam}}</p> <input type="nubmer" v-model="multi"> </div> </template> <script> export default { name: "about", data: function () { return { multi: 0 }, computed:{ countByParam:function () { return this.$store.getters.countByParam(this.multi); } }, ...... } </script>
在input中输入数字,就会结合当前的Vuex中的count和这个数字计算出倍数。
使用传参数的时候,不会缓存,每次都会重新计算。
还有一些辅助方法如mapGetters 辅助函数可以查看文档。
action
actions中也是一系列方法,这些方法不通过直接操作数据,而是通过commit mutation来操作。
const store = new Vuex.Store({
state:{
count: 0
},
mutations:{
increase: function (state) {
state.count++;
},
decrease: function (state) {
state.count--;
},
reset: function (state) {
state.count = 0;
}
},
actions:{
increaseBy1:function (context) {
context.commit("increase");
}
}
});
在index.vue
里,改用action
来让count+1:
methods:{
increase: function () {
this.$store.dispatch("increaseBy1");
},
decrease: function () {
this.$store.commit("decrease");
}
}
使用action
的时候,调用Vuex
对象的方法dispatch
,参数名称为actions
里定义的方法名称。
猛一看感觉好像吃饱了撑的,能直接去使用mutations
,为何还要包一层action
来commit mutation
,似乎又封装了一层,多此一举。
其实仔细的话,WebStorm已经在dispatch
方法下边划出了波浪线,dispatch实际上返回的是一个Promise对象,还可以使用回调函数。
mutations
内部不能执行异步操作,而actions
就可以,如果直接就套一层,那就没意思了。核心在于异步操作,比如设置延迟一秒再去增加1,可以返回一个Promise对象:
actions:{ increaseBy1:function (context) { return new Promise(resolve=>{ setTimeout(() => { context.commit("increase"); resolve(); }, 1000); }); } }
将业务逻辑写在一个Promise
对象中即可。
之后在index.vue里使用的时候可以加上回调函数:
increase: function () { this.$store.dispatch("increaseBy1").then(() => { console.log("成功完成+1") }) },
store.dispatch
可以处理被触发的action
的处理函数返回的Promise
,并且store.dispatch
也返回Promise
对象,意味着actions
能够连续调用,就像在一个getter
方法里调用另一个getter
方法一样(原理不同)。
这就是异步修改Vuex状态,而且修改成功与否还能够回调执行其他动作。actions
实际上就是异步版本的mutations
。
在之前commit mutations
的时候,提到过可以使用对象形式,传入type
和参数名称的对象,actions
也一样支持,这是官网的例子:
// 以载荷形式分发 store.dispatch('incrementAsync', { amount: 10 }) // 以对象形式分发 store.dispatch({ type: 'incrementAsync', amount: 10 })
module
可能注意到,在actions
中,方法的参数名称叫做context
,虽然名称是任意起的,但是这暗示着这个不是普通的state对象。
如果项目很大,把所有的选项都写在一个store对象里并不好,modules就是把Vuex区分成几个部分,针对每一个部分只要进行命名,然后注册在Vuex实例化的过程中,就可以单独使用。
比如简单的例子:
const moduleA = { state: { name: "MA" } }; const moduleB = { state: { name: "MB" } }; const mainStore = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }); //moduleA的state对象 console.log(mainStore.state.a); //访问moduleA和moduleB的name属性 console.log(mainStore.state.a.name); console.log(mainStore.state.b.name);
此时如果在moduleA
或者moduleB
中添加getters
和mutations
,其中的方法的第一个参数state
是自己所属的模块的对象,第二个参数依然是实例的getters
,还可以有第三个参数,就是根节点,由于modules
也是一个选项,所以还可能会有根节点。
此外,还要注意命名空间的问题,默认情况下,各个模块里的方法名称,都会统一注册到Vuex实例内部的全局空间去。看一个例子:
const moduleA = { state: { name: "MA" }, getters:{ getname:function (state) { return "This is module " + state.name + "!"; } } }; const moduleB = { state: { name: "MB" }, getters:{ getname:function (state) { return "This is module " + state.name + "!"; } } }; const store = new Vuex.Store({ state:{ name: 'masterModule' }, modules: { a: moduleA, b: moduleB } });
这个例子如果运行起来,Vue会报错:
vuex.esm.js:765 [vuex] duplicate getter key: getname
这是因为Vuex会把模块A和模块B的getters
函数都注册到store.getters
下边,但是两个函数的名称相同,就有了冲突。在外部使用Vuex实例的时候,是不区分模块的,比如index.vue
里是这么调用的:
computed:{ getMA:function () { return this.$store.getters.getname; } }
此外,根模块的name='masterModule'
想要访问,就要通过getters
的第三个参数根节点。
模块命名空间就是给模块对象再加上一个namespaced: true
,之后其中的getter
、action
及mutation
,都会自动调整命名,这个可以参考官方的文档。
还记得本节开头的actions
中函数的参数吗,和getters
及mutations
不同,模块内部的action
,局部状态通过context.state
暴露出来,根节点状态则为context.rootState
。
最后写一个简单一点的完整例子,包含两个模块的Vuex实例:
const module1 = { namespaced:true, state: { name: "Module1", count: 0 }, getters:{ //故意起的和另一个模块的方法名称一样 //使用rootState访问根节点 getname: function (state, getters, rootState) { return "This is " + state.name + "from " + rootState.name; }, //故意起的和另一个模块的方法名称一样 //使用rootState访问根节点 gettotal: function (state, getters, rootState) { return state.count + rootState.count; } }, //模块内部默认引用自己的state mutations:{ increase1: function (state) { state.count += 1; } }, //actions内部引用context也是自己的$store对象 //context.state是当前模块的state //context.rootState是根模块的state actions: { asycnincrease: function (context) { return new Promise(resolve => { console.log(context.state.name); console.log(context.rootState.name); setTimeout(()=>{ context.commit("increase1"); resolve(); },1000) }) } } }; const module2 = { namespaced:true, state: { name: "Module2", count: 0 }, getters:{ getname: function (state, getters, rootState) { return "This is " + state.name + "from " + rootState.name; }, gettotal: function (state, getters, rootState) { return state.count + rootState.count; } }, mutations:{ increaseby1: function (state) { state.count += 1; } }, actions: { asycnincreaseb: function (context) { return new Promise(resolve => { console.log(context.state.name); console.log(context.rootState.name); setTimeout(()=>{ context.commit("increaseby1"); resolve(); },1000) }) } } }; const store = new Vuex.Store({ state:{ name: 'masterModule', count: 0 }, //在Vuex实例中注册模块,模块的键就是对模块对象的引用 modules: { ma: module1, mb: module2 }, //故意定义了一个和module1名称一样的方法 mutations:{ increase1:function (state) { state.count++; } } });
定义好之后,关键是在带有命名空间的情况下如何使用这个Vuex实例,看index.vue
:
<template> <div> <p>A的名称: {{ getMA }} <br>A count: {{countA}}</p> <p>A count + Total Count = {{at}}</p> <p> <button @click="handle">A+1</button> </p> <p> <button @click="handle2">异步A+1</button> </p> <p>B的名称: {{ getMB }} <br>B count: {{countB}}</p> <p>B count + Total Count = {{bt}}</p> <p> <button @click="handleb">+1</button> </p> <p> <button @click="handleb2">B异步+1</button> </p> <p>全局模块的名称: {{mastername}} <br> 全局模块的count: {{mastercount}}</p> <p> <button @click="masterincrease">全局变量+1</button> </p> </div> </template> <script> export default { name: "about", computed: { //使用模块的getters,此时this.$store.getters是一个对象,通过命名空间找到具体的模块的getname函数。 getMA: function () { return this.$store.getters['ma/getname']; }, getMB: function () { return this.$store.getters['mb/getname']; }, //访问state,通过在Vuex实例中注册的名称找到对应的state countA: function () { return this.$store.state.ma.count; }, countB: function () { return this.$store.state.mb.count; }, //访问Vuex根节点的属性,直接使用this.$store.state mastercount: function () { return this.$store.state.count; }, mastername: function () { return this.$store.state.name; }, at: function () { return this.$store.getters['ma/gettotal']; }, bt: function () { return this.$store.getters['mb/gettotal']; }, }, methods: { //使用mutations,与getters类似,通过命名空间和函数名构成的键对应到具体函数 handle: function () { return this.$store.commit('ma/increase1'); }, handleb: function () { return this.$store.commit('mb/increaseby1'); }, //使用action,与使用mutation类似,dispatch参数的值是命名空间构成的键 handle2: function () { this.$store.dispatch('ma/asycnincrease').then(() => { console.log("异步A+1完成"); }); }, handleb2: function () { this.$store.dispatch('mb/asycnincreaseb').then(() => { console.log("异步B+1完成"); }) }, //使用根节点的amutations,无需命名空间,直接使用 masterincrease: function () { this.$store.commit("increase1"); } } } </script> <style scoped> div { font-size: 2rem; text-align: center; line-height: 0.9; } </style>
Vuex使用注意事项
官网在这里有对项目结构的介绍,很不错,复制过来加点自己的想法:
- 应用层级的状态应该集中到单个
store
对象中。所谓应用层级,就是全局的状态,集中到一个store对象中,对于各个组件自己保存的状态,则无需都写入到store对象中。 - 提交
mutation
是更改状态的唯一方法,并且这个过程是同步的。凡是要同步修改数据,一定要通过mutation方法,不要采取其他的hack方法。我自己试了在mutation
中也可以强行写异步的方法,但是估计会导致阻塞吧。 - 异步逻辑都应该封装到
action
里面。凡是需要异步修改状态的,必须要全部写在action
中,不要写到外边来。