在之前组件间通信的时候提到过,可以用一个类似中介一样的东西收集和保存所有状态,让所有的组件通用。如果在Vue之外定义一个对象,然后在Vue中引用对象,这个对象也会变成响应式的。
在简单的应用中,一般就使用简单的一个对象集中存储一下数据即可,比如一个全局变量。但是在复杂的应用里,就需要用一个东西来集中保存状态。其实可以把Vuex认为是一个数据库,可以向其中写入数据,获取数据(自动的而且是响应式的),Vuex帮你把状态和改变都记录下来。
Vuex和Vue Router一样都是由官方维护的库,文档地址。
- 安装Vuex和导入
- 初步配置Vuex和使用数据
- Vuex对象
- Vuex使用注意事项
安装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
- mutation
- getter
- action
- module
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
中,不要写到外边来。