Vue现代化使用方法(四)--Vuex
在组件内可以通过data属性共享数据,父子组件也可以通过props进行数据共享,但如果是兄弟跨组件之间的数据共享,就要借助Vuex,Vuex类似大树的主干,各个组件类似一个个独立的树枝,组件之间通过Vuex形成一种联系,只要把需要共享的数据放到Vuex中,所有相关组件都可以访问引用,这样就能形成跨组件的数据共享。
通过npm进行安装
npm install --save vuex
基本组成
按Vuex的设计思路,其核心(Store)是包括下面四部分:
- state
- getters
- mutations
- actions
state
// 在index.js页面做如下改造
import Vuex from 'vuex'; // 引入Vuex
Vue.use(Vuex); // 启用Vuex
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
}); // 设定一个公共仓库
let vm = new Vue({
el: "#app",
store, // 引入建立的仓库
render: h => h(app)
});
...
// 在index.vue的计算属性中添加下面代码
computed: {
count () {
return this.$store.state.count
}
}
...
// 在index.vue的模板中添加如下内容,引入之前创建的header.vue组件
index.vue中的值:{{count}}
...
// 在header.vue组件也一样添加相关计算属性,并修改模板中的内容
header.vue中的count值:{{count}}
...
// 这时页面就渲染为
index.vue中的值:0
header.vue中的count值:0
通过上例我们可以看到在index.js设定的公共仓库(store)中的state内容,在index.vue和header.vue中共享,通过调用this.$store.state.count来引用,这样就做到了跨组件数据共享
把数据放到计算属性中,
在上例中,我们是通过this.$store.state.count对仓库中值进行访问,这种写法是显得有些冗余,Vuex在这里提供了mapState函数来优化了这个问题
// 利用mapState,可以简化对仓库中值的调用,计算属性中的其他值,正常使用不受影响,添加了这个方法只影响了对仓库中值的取用
// 先引入这个方法
import { mapState } from 'vuex';
...
// 在计算属性中用这个方法包裹
computed: mapState({
count (state) {
return state.count;
},
fullName () {
return `${this.firstName}-${this.lastName}`;
}
}),
...
// 利用箭头函数还可以在简化下当前代码
count: state => state.count,
fullName () {
return `${this.firstName}-${this.lastName}`;
}
...
// 借助mapState可以使用字符串数组来再简化代码,调用this.count时 映射 store.sate.count
computed: mapState(['count']),
...
// 如果名词不一致可以使用对象形式,在模板中使用{{co}}
...mapState({
co: 'count'
})
...
// 不过这时我们不好添加其他的计算属性了,在这里可以简化使用对象扩展来解决这个问题
computed: {
fullName () {
return `${this.firstName}-${this.lastName}`;
},
...mapState(['count'])
},
getters
getters就是针对state的计算属性
// index.js中进行下面内容添加
const store = new Vuex.Store({
state: {
count: 0,
name: 'lin ken',
address: '深圳XX区XX街道',
},
getters: {
userInfo (state) {
return `姓名:${state.name}; 住址:${state.address}`;
}
}
});
...
// index.vue计算属性添加下面内容
userInfo () {
return this.$store.getters.userInfo;
},
...
// index.vue模板添加如下内容
{{userInfo}}
与mapState类似,getters也有mapGetters使用方式与mapState类似
// 引入mapGetters
import { mapState,mapGetters } from 'vuex';
...
// 在计算属性中使用扩展对象写法把相关getters写到计算属性中
fullName () {
return `${this.firstName}-${this.lastName}`;
},
...mapGetters(['userInfo']),
...mapState(['count']),
...
// 如果不希望使用userInfo这个名词,mapGetters可以使用对象形式,替换相关名词
...mapGetters({
cusInfo: 'userInfo'
})
...
// 模板中可以使用
{{cusInfo}}
mutation
mutation是Vuex设定唯一可以进行对state值进行修改的地方,Vuex应该在这里对这些值的变化做了相关监听跟踪
// index.js中的公共仓库添加如下代码
mutations: {
increment (state) {
state.count++
}
}
...
// index.vue的模板页做如下调整
index.vue中的值:{{co}}
...
// index.vue中添加methods
addCount () {
this.$store.commit('increment'); // 通过commit调用mutations中的increment方法
}
这时点击页面就会发现在index.vue和header.vue中引用的count值一起发生了更改
// 通过commit调用mutations时,还可以传递参数
// 在index.js中的方法接收传入参数
increment (state, payload) {
state.count += payload.addNum;
}
...
// 在index.vue中的调用方法传递相关值
addCount () {
this.$store.commit('increment', {addNum:100}); // 调用mutations
}
...
// 如果使用对象形式进行传值,还可以使用下面的简写方式
// commit方法自动识别type类型,所以也就不要使用type来传相关值
this.$store.commit({
type: 'increment',
addNum:100
})
在组件内通过commit触发mutations时,我们是使用字符串increment,这就隐含会有一些问题当项目比较大时,同时协助的人比较多时,页面中随便调用一个mutations可能是没人知道做什么用的
// 在项目目录建立mutation-types.js
export const INCREMENT = 'INCREMENT'; // 在index.js的公共仓库中针对count增加的mutations
...
// 在index.js引入这个文件
import { INCREMENT } from './mutation-types';
...
// 在store中的mutations做如下修改
mutations: {
[INCREMENT] (state, payload) {
state.count += payload.addNum;
}
}
...
// 在index.vue中引入mutation-types.js,调用时也做相关修改
addCount () {
this.$store.commit({
type: INCREMENT,
addNum:100
})
}
这样在一个公共的地方管理mutations,如果注释写的清晰,项目人员可以很明晰知道各个mutations的作用。
同mapState,mapGetters,针对mutations,也有一个mapMutations
import { mapState, mapGetters, mapMutations } from 'vuex';
...
// 在methods中添加相关内容,mapMutations要用在methods中
// 如果不需要传值,直接使用this.INCREMENT();
methods: {
...mapMutations([INCREMENT]),
addCount () {
this.INCREMENT({addNum: 100});
}
mutations必须是同步函数
看官方的解释似乎是因为使用回调函数后无法明确的监听到state的变化,所以针对mutations的函数只能是同步函数,针对这个问题Vuex设定了actions
actions
- actions提交的是mutations,通过dispatch触发
- actions支持异步函数
// 在index.js中的公共仓库添加如下代码
// context是一个与实例对象一致的对象,可以通过 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
actions: {
increment (context, payload) {
context.commit(INCREMENT, payload);
}
}
...
// 借用参数解构,代码可以做如下优化
// 获取state,getters可以一样使用参数解构
increment ({commit}, payload) {
commit(INCREMENT, payload);
}
...
// 在index.vue的页面方法做如下改造,actionst通过dispatch触发
addCount () {
this.$store.dispatch('increment', {addNum: 100});
}
...
// 与commit类似,也可以使用下面的简写方式
this.$store.dispatch({type: 'increment', addNum: 100});
...
// 可以使用mapActions简化代码
// 引入mapActions函数
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
...
// 在methods下做下面修改
...mapActions(['increment']),
addCount () {
this.increment({addNum: 100});
}
在我的理解中,我认为actions是对mutations的增强,所以在实际使用中,更加习惯在actions中添加相关逻辑,并触发mutations来更新state
Modules
上面例子中我们都是在index.js的公共仓库中store来进行相关操作,可以预期到,当项目非常大时,把所有组件用到的state都放在一个stroe上会造成这个store过大,为了解决这个问题,可以把stroe按功能进行拆分多个小的stroe然后在组合到一起进行使用。
// 在index.js,新建两个对象userStore和schoolStroe用来假定是用来存放用户信息和学校信息
const userStore = {
state: {
name: 'lin ken from userStore',
address: '深圳XX区XX街道',
}
};
const schoolStore = {
state: {
department: '软件学院',
classroom: 'C509'
}
};
// 在Vuex.Store的modules下,使用modules添加刚刚新建的子仓库
const store = new Vuex.Store({
modules: {
userStore,
schoolStore
},
state: {
count: 0,
},
getters: {
userInfo (state) {
return `姓名:${state.name}; 住址:${state.address}`;
}
},
mutations: {
[INCREMENT] (state, payload) {
state.count += payload.addNum;
}
},
actions: {
increment ({commit}, payload) {
commit(INCREMENT, payload);
}
}
});
...
// 在index.vue的计算属性中做如下改造,引入新建的子仓库
userStoreInfo () {
return `姓名:${this.userStore.name};地址:${this.userStore.address}`;
},
schoolStoreInfo () {
return `学院:${this.schoolStore.department};班级:${this.schoolStore.classroom}`;
},
...mapState(['count', 'schoolStore', 'userStore'])
在实际项目中,我们可能会把子仓库放到不同的文件下进行管理
// 在项目目录下新建两个文件userStore.js和schoolStore.js
// userStore.js内容如下
export const userStore = {
state: {
name: 'lin ken from userStore',
address: '深圳XX区XX街道',
}
};
...
// schoolStore.js内容如下
export const schoolStore = {
state: {
department: '软件学院',
classroom: 'C509'
}
};
...
// 在index.js目录下引入刚刚新建的子仓库
import {userStore} from './userStore';
import {schoolStore} from './schoolStore';
通过这种拆分管理,可以很方便的把各个子仓库组合在一起,易于维护和使用。Vuex在实现子仓库时只会把state按模块名词进行划分,其他的getters、actions是会合并在一起使用不会按模块划分。
// userStore.js做如下改造
export const userStore = {
state: {
name: 'lin ken from userStore',
address: '深圳XX区XX街道',
},
getters: {
rootCount (state, getters, rootState) {
return rootState.count;
}
},
mutations: {
changeName (state, payload) {
state.name = payload.name;
}
},
actions: {
changeName ({commit}, payload) {
commit('changeName', payload);
}
}
};
...
// index.vue的计算属性中改造如下
...mapGetters(['rootCount']),
userStoreInfo () {
return `姓名:${this.userStore.name};地址:${this.userStore.address};from rootState:${this.rootCount}`;
}
...
// index.vue的方法改造如下
...mapActions(['increment', 'changeName']),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
通过上例子可以看出如果我们要调用子仓库的相关state需要添加仓库名:this.userStore.name/this.schoolStore.classroom,但是如果我们要调用子仓库的getters、mutations、actions时无须添加子仓库名,这时信息会合并在一起:
this.rootCount/this.changeName,这样会造成子仓库的getters,mutations,actions重名的情况
// 在schoolStore.js做如下修改
export const schoolStore = {
state: {
department: '软件学院',
classroom: 'C509'
},
getters: {
rootCount (state) {
return state.classroom;
}
},
mutations: {
changeName (state, payload) {
state.department = payload.name;
}
},
actions: {
changeName ({commit}, payload) {
console.log('school store');
commit('changeName', payload);
}
}
};
...
// 在userStore.js做如下修改
actions: {
changeName ({commit}, payload) {
console.log('user store');
commit('changeName', payload);
}
}
这时页面会提示rootCount是一个重复的getters关键字,但是针对重复的mutations和actions却无法检测出来,这个时候你点击页面的按钮会发现,我们在index.vue设定的要执行changeName的方法,本意上你可能想要执行userStore.js中actions,但因为无意间在schoolStore.js中也声明了一个同名actions,这样就造成了两个actions同时触发(通过控制台我们能验证这一点),类似这种没有明确报错,完全因为人为原因造成的无意错误,在开发过程中是极其难维护的,尤其是在多人协作的项目中,所以总是建议针对mutations和actions定义的名词(尤其是mutations,因为一般是在组件内直接触发actions,actions重名的可能性要比mutations小)要存放在一个独立的常量列表中,类似mutations-type.js。
除了把相关信息名独立声明外,还可以利用命名空间来解决这个问题,因为默认getters,mutations和actions这些值是绑定在全局声明中,这样可以方便调用。
// 在userStore.js中添加namespced为true,开启这个命名空间
export const userStore = {
namespaced: true,
...
// 在index.vue页面要做如下调整
// 这样页面使用的this.rootCount就指向userStore下的rootCount
...mapGetters({
'rootCount': 'userStore/rootCount'
}),
...
// 如果不像使用对象的形式还是保持使用字符串数组,可以如此引用
...mapActions(['increment', 'userStore/changeName']),
addCount () {
this['userStore/changeName']({name: 'Rede'});
this.increment({addNum: 100});
}
虽然this['userStore/changeName']使用起来并不是很方便,但是这样的用法可以明确的指出这个方法的所属,如果不想使用这么麻烦的写法,还可以指定命名空间:
// 把所有绑定都绑定到userStore这个子仓库下
...mapActions('userStore/', ['increment', 'changeName']),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
这样对changeName的调用就可以直接调用userStore下的内容,还可以借助createNamespacedHelpers来在全局自动添加
import { mapState, mapGetters, mapMutations } from 'vuex';
import { createNamespacedHelpers } from 'vuex';
const { mapActions } = createNamespacedHelpers('userStore/');
...
// 下面的代码和最开始没有加入命名空间时一致,看起来是改动量最小的
...mapActions(['increment', 'changeName']),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
不过因为这种添加是针对所有的绑定信息,所以increment也会指定到userStore这个子仓库中,如果该子仓库没有这个方法就会报错,如何使用这个内容需要好好考虑下,不过如果想一起都要,也可以借助对象的形式来指定扩展信息。
// 把头部信息的引用改回去
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
...
// 相关内容做如下改造
// 这样使用即保证了不会影响全局命名,又能指定特定方法调用指定子仓库,而且也方便随时切换
...mapActions({
increment: 'increment',
changeName: 'userStore/changeName'
}),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
另外也可以针对这个同名的changeName,我们可以根据文件名的不同,分别定义为
USER_CHANGENAME,
SCHOOL_CHANGENAME
如何使用,就看你怎么考虑了。
在子仓库中,如果要获取主仓库中的state,getters可以传rootState,rootGetters这个参数
// getters下,rootState是第三个参数,rootGetters是第四个参数
rootCount (state, getters, rootState, rootGetters)
// actions中,可以直接使用参数解构的形式,设定这个值
changeName ({commit, rootState, rootGetters}, payload)
动态创建子仓库
除了直接声明好使用的子仓库,还可以动态的生成
// 在index.vue通过registerModule创建一个子仓库
mounted () {
this.$store.registerModule('myModule', {
state: {
info: 'from new module'
}
});
},
...
// 在相关方法中可以直接调用
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
this.info = this.$store.state.myModule.info;
this.$store.unregisterModule('myModule');
console.log(this.$store.state.myModule);
}
这种临时的子仓库在子组件之间用来临时通信是非常有用的,使用完成还可以调用unregisterModule直接删除,为了避免临时创建的子仓库影响到静态仓库,还可以在指定子仓库创建下一级仓库
mounted () {
this.$store.registerModule('nested', {
state: {
info: 'nested'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
info: 'from new module'
}
});
},
...
// 直接调用
console.log(this.$store.state.nested.info);
console.log(this.$store.state.nested.myModule.info);
动态仓库是临时创建的,碰到同名的会直接覆盖。
// 第二个myModule会覆盖第一个,myModule中的state只包含一个name值
mounted () {
this.$store.registerModule('nested', {
state: {
info: 'nested'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
info: 'from new module'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
name: 'myModule'
}
});
},
如果希望这些动态创建的子仓库可以叠加,可以添加preserveState参数
// 这时创建的myModule的state就包含info和name两个值
this.$store.registerModule(['nested', 'myModule'], {
state: {
info: 'from new module'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
name: 'myModule'
}
}, { preserveState: true });
仓库的复用
如果我们对一个组件进行复用,
插件
Vuex支持自定义插件,通过插件可以监听状态的变化。
// 在index.js中新建一个插件
// store是Vuex插件的唯一参数
// subscribe是在mutations发生变化时调用
// subscribeAction是action发生变化时调用
const myPlugin = store => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
console.log('from myPlugin');
console.log(mutation);
});
store.subscribeAction((action, state) => {
console.log('from myPlugin');
console.log(action);
})
};
...
// 在初始化方法中通过plugins直接引用
const store = new Vuex.Store({
...
plugins: [myPlugin]
});
针对input的优化
Vuex中会强制要求只通过mutations去修改state的值,如果我们把某个input的value值绑定为一个针对state的计算属性,那如何去修改对应在state中的值?
// 在index.vue的模板中输入如下内容
...
userStoreInfo () {
return `${this.userStore.name}`;
},
这时如果我们尝试修改input中的内容,控制台会直接报错提示我们没有设置setter,解决方案就是设置set方法,并通过actions去修改这个对应的state
userStoreInfo: {
get () {
return `${this.userStore.name}`;
},
set (value) {
this.changeName({name: value});
}
},
官网上还提到另外一种绑定value去监听input方法,再更新的方案,感觉没这样直接
d