Vuex Module

2,944 阅读16分钟

开始之前

本篇只是本人对 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
    }
}

是的访问全局只需要提供rootStaterootGetters参数就好,而分发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({
      astate => state.a,
      bstate => state.b
    })
  },
  methods: {
    // 在 `some/nested/module`上下文中 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

这个例子我有一个疑问:如果mapStatemapActions是基于'some/nested/module'上下文的话,那如果我这个组件内还需要使用其他命名空间的模块该怎么办呢?有的同学可能会说:

    const { mapState, mapActions } = createNamespacedHelpers('otherSome/nested/module')

再定义一个上下文不就好了吗?但两个上下文返回的都是同样的mapStatemapActions,我在使用mapActions时,你怎么知道我是在那个上下文中查找呢?

后来想了想我这个疑问是否成立?因为我觉得一个模块store应该始终是效力于一个功能组件的。但是不保证没有极端的情况出现,如果真有这种需求的话,该怎么实现?有经验的同学可以教我一下。

动态注册命名空间模块

如果有业务需求需要我们动态注册模块,我们可以使用 store.registerModule 方法注册模块:

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested''myModule'], {
  // ...
})

动态注册的前提是你的Store已经被你创建了。

之后就可以通过store.state.myModulestore.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都会是一个全新的函数返回。

最后

给某些同学一些建议:做笔记、总结这种东西一定是要你自己先学习一遍,然后理解过后的记录,并非是把人家文档的东西按部就班放到你的笔记当中,这样做的意义何在呢?骗点击的沙雕网友我们就不评价了,而且人家文档的东西始终是最新的,而且也有持续更新。复制 - 粘贴 - 发布的这类沙雕网友拜托你们不要浪费大家的时间了。净化学习环境从我做起!

当然一篇总结总是避免不了会用到原文档的一些例子。