开始之前
本篇只是本人对 module 的学习总结和理解以及一些避免坑的意见,不一定可以用在你的项目当中,即使你要使用,建议你先参考官方对比下前后文。
另外,module 是基于 vuex 即 store 状态的模块化管理方案,所以本篇是针对有过 store 使用经验的同学的一篇仅供参考的个人总结,如果你还不会 store 你得抓紧了! 或者你可以参考 大宏说 老师的《Vuex白话教程第六讲:Vuex的管理员Module(实战篇)》
模块的局部状态
模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象:
const moduleA = {
state: { count:10 },
getters:{
filterCount(state){
return state.count * 2; // 20;
}
},
mutations:{
add(state){
state.count * 2; // 20;
}
},
}
action则是通过context.state暴露出来:
actions:{
incrementIfOddOnRootSum(context){
context.state.count // 10
}
}
模块内部访问全局的状态
action 可以通过rootState获取到根节点的状态:
actions:{
incrementIfOddOnRootSum( { state, rootState } ){
rootState.xx // 根节点的xx
}
}
getter 接受根节点状态是通过第三个参数暴露出来:
getters:{
sumWithRootCount(state, getters, rootState){
//state 是模块内部的状态
// getters 模块内部的其他getter
// rootState 是全局的状态
}
}
命名空间的概念
如果模块不使用命名空间的话,默认情况下模块内部的 getter, action 和 mutation是注册在全局全局命名空间的,这样的坏处是:
store:{
state:{
count:18,
},
mutations:{
setCount(state){
state.count / 2;
console.log(state.count) // 9
}
},
modules:{
a:moduleA
}
}
moduleA:{
state:{
count:10,
},
mutations:{
setCount(state){
state.count * 2;
console.log(state.count)//20
}
}
}
在提交 moduleA 的 mutation 时:
this.$store.commit('setCount');
// 猜猜会打印啥?
// 9 和 20 这是因为前面所说的模块内部的getter,action和mutation是注册在全局全局命名空间的。
//所以上面的例子中全局命名空间里有2个名为setCount的mutation,然后他们都被触发了。
开启命名空间
想要让模块内部的 getter, mutation , action只作用域当前局部模块内的话可以给模块添加namespaced属性:
modules:{
moduleA:{
namespaced:true,
state:{...}, //state还是仅作用于当前模块内部
getter:{
isAdmin(){...}
},
mutations:{
login(){...}
},
actions:{
getToken(){...}
}
}
}
当开启命名空间的模块被注册后它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名,所以触发路径也有所改变:
store.getters['moduleA/isAdmin']; // 命名空间模块内部的getter
store.dispatch('moduleA/getToken'); // 命名空间模块内部的action
store.commit('moduleA/login'); // 命名空间模块内部的mutation
命名空间模块的嵌套
文档里也有说模块内是还可以嵌套模块的大概意思就是:
modules:{
moduleA:{
state:{...},
mutations:{...},
// 嵌套模块
modules:{
mypage:{
state:{...},
getters:{
profile(state){}//因为嵌套模块没有自己命名空间,所以就自动继承了父命名空间,所以就可以这样触发这个getter:store.getters['moduleA/profile'];
}
},
// 进一步嵌套命名空间
posts:{
namespaced:true,//开启命名空间
state:{...},
getters:{
popular(){...}//前面我们说过,开启命名空间的模块它所有的getter、action、mutation都会自动根据模块的路径调整命名 -> store.getters['moduleA/posts/popular']
}
}
}
}
}
嵌套模块继承命名空间后带来的问题
接着上面继承命名空间的例子:
modules:{
moduleA:{
state:{...},
mutations:{...},
// 嵌套模块
modules:{
mypage:{
state:{...},
getters:{
profile(){...}
}
}
}
}
}
如果我现在要触发profile这个getter我可以这样:
store.getters['moduleA/profile'];
因为即使是嵌套的模块但mypage没有自己的命名空间所以继承了父命名空间,所以这样触发看上去没有问题。
问题来了⚠️
如果父命名空间内也有一个名为 profile 的getter:
modules:{
moduleA:{
state:{...},
getters:{
profile(state){...}
}
mutations:{...},
// 嵌套模块
modules:{
mypage:{
state:{...},
getters:{
profile(){...}
}
}
}
}
}
这个时候如果再执行:
store.getters['moduleA/profile'];
会是什么结果呢?大家可以自己动手试一试,加深一下印象。
带命名空间的模块和全局命名空间模块的一些互动
获取或修改全局状态:
如果你希望使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
modules: {
foo: {
namespaced: true,
getters:{
someGetter(state, getters, rootState, rootGettrers){
// state 是当前模块内部是状态
// getters 是当前模块内部的 getters
// rootState 是全局下的状态
// rootGettrers 是全局下的 gettrers
}
},
actions:{
someAction( { getters, rootGetters}){
// getters 当前模块内部的 getters,
// rootGettrers 是全局下的 gettrers
}
}
}
}
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可:
action:{
someAction( { dispatch, commit}){
dispatch('someOtherAction');// 分发当前模块内名为someOtherAction的action
dispatch('someOtherAction', null, { root: true })// 分发全局名为someOtherAction的action
commit('someMutation') // 提交当前模块内名为someMutation的mutation
commit('someMutation', null, { root: true }) // 提交全局名为someMutation的mutation
}
}
是的访问全局只需要提供
rootState和rootGetters参数就好,而分发action或提交mutation只需要将{ root: true }作为第三个参数就好。
将模块内部的action注册到全局:
若业务需要在带命名空间的模块中注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
在组件中使用带命名空间的模块
官方给了两种方案,这里我使用命名空间辅助函数的写法,另一种写法有兴趣的同学可以去参考一下
这个例子官方其实已经给的很简洁直观了所以我们直接看:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module`上下文中 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module`上下文中 中查找
...mapActions([
'foo',
'bar'
])
}
}
这个例子我有一个疑问:如果mapState和mapActions是基于'some/nested/module'上下文的话,那如果我这个组件内还需要使用其他命名空间的模块该怎么办呢?有的同学可能会说:
const { mapState, mapActions } = createNamespacedHelpers('otherSome/nested/module')
再定义一个上下文不就好了吗?但两个上下文返回的都是同样的mapState和mapActions,我在使用mapActions时,你怎么知道我是在那个上下文中查找呢?
…
后来想了想我这个疑问是否成立?因为我觉得一个模块store应该始终是效力于一个功能组件的。但是不保证没有极端的情况出现,如果真有这种需求的话,该怎么实现?有经验的同学可以教我一下。
动态注册命名空间模块
如果有业务需求需要我们动态注册模块,我们可以使用 store.registerModule 方法注册模块:
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
动态注册的前提是你的Store已经被你创建了。
之后就可以通过store.state.myModule和store.state.nested.myModule访问模块的状态。
卸载动态模块
之后不需要模块时可以使用store.unregisterModule(moduleName)来动态卸载模块来保证性能
注意:不能使用此方法卸载静态模块(即创建 store 时声明的模块)
模块的复用
模块重用,官方给的场景是:
1、创建多个store,他们共用同一个模块
2、在一个store 中多次注册同一个模块
说的可能比较抽象,我们来一个简单的例子:
moduleA.js
const moduleA = {
state:{
count:10
},
mutations:{
changeCount(state){
state.count = 20;
}
}
}
export default moduleA;
store.js
const store = new Vuex.Store({
state:{...}
mutations:{...},
modules:{
a:moduleA,
b:moduleA,
c:moduleA
}
})
此时modules对象里的a b c 都引用自moduleA模块;
我们再来建3个组件,然后分别引用a b c这3个模块的实例:
test1.vue
<template>
<div>
{{count}}
</div>
</template>
<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('a')
export default {
computed: {
...mapState({
name:state => state.count
})
}
}
</script>
test2.vue
<template>
<div>
{{count}}
</div>
</template>
<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('b')
export default {
computed: {
...mapState({
name:state => state.count
})
}
}
</script>
test3.vue
<template>
<div>
{{count}}
</div>
</template>
<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('c')
export default {
computed: {
...mapState({
name:state => state.count
})
}
}
</script>
3个组件的count都等于10,因为modulea b c 都引用自moduleA模块;
此时,如果我们在test1组件里提交moduleA的mutation:
test1.vue
<template>
<div>
{{count}}
<input type="button" value="click" @click="changeCount">
</div>
</template>
<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState,mapMutations } = createNamespacedHelpers('a')
export default {
methods:{
...mapMutations([changeCount])
}
computed: {
...mapState({
name:state => state.count
})
}
}
</script>
此时,只要我们一提交changeCount,test1 test2 test3 组件里的count都会被改为20;
原因:
当一个模块被定义,模块可能被用来创建多个实例,这时如果模块仍是一个纯粹的对象,则所有实例将共享引用同一个数据对象!这就是模块间数据互相污染的问题。
解决模块复用带来的互相污染问题
为了解决互相污染我们可以使用一个函数声明来返回模块的状态:
const MyReusableModule = {
state () {
return {
count: 10
}
},
// mutation, action 和 getter 等等...
}
通过为 state 声明一个初始数据对象的函数,且每次创建一个新实例后,我们能够调用 state 函数,从而返回初始数据的一个全新副本数据对象。
此番借鉴Vue 组件内的 data
大致意思就是让state以函数声明式返回状态,这样不管模块被实例化多少次,每次实例化时模块内部的state都会是一个全新的函数返回。
最后
给某些同学一些建议:做笔记、总结这种东西一定是要你自己先学习一遍,然后理解过后的记录,并非是把人家文档的东西按部就班放到你的笔记当中,这样做的意义何在呢?骗点击的沙雕网友我们就不评价了,而且人家文档的东西始终是最新的,而且也有持续更新。复制 - 粘贴 - 发布的这类沙雕网友拜托你们不要浪费大家的时间了。净化学习环境从我做起!
当然一篇总结总是避免不了会用到原文档的一些例子。