modules

由于使用单个状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将 store 分割成模块(Module)。每个模块都拥有自己的 statemutationsactionsgetters,甚至是嵌套子模块。最后在根 store 采用 modules 这个设置项将各个模块汇集进来,如示例代码 6-7-1 所示。

示例代码6-7-1 Modules
const moduleA = {
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
}
const moduleB = {
    state: { ... },
    mutations: { ... },
    actions: { ... }
}
const store = Vuex.createStore({
    modules: {
        a: moduleA,
        b: moduleB
    }
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

为了更好地理解,举个例子,对于大型的电商项目,可能有很多个模块,例如用户模块、购物车模块、订单模块等。如果将所有模块的程序逻辑都写在一个 store 中,肯定会导致这个代码文件过于庞大而难以维护,如果将用户模块、购物车模块和订单模块单独抽离到各自的 module 中,就会使代码更加清晰易读,便于维护。

可以在各自的 module 中定义自己的 store 内容,代码如下:

...
const moduleA = {
    state: { count: 0 },
    mutations: {
        increment(state) {
            // 'state' 可以获取当前模块的state状态数据
            state.count++
        }
    },
    getters: {
        doubleCount(state) {
            return state.count * 2
        }
    },
    actions:{
        incrementAction(context){
            context.commit('increment')
        }
    }
}
...

在默认情况下,模块内部的 actionmutationgetters 注册在全局命名空间中,可以不受 module 限制,而 statemodule 内部,它们可以通过下面这种方式获取到:

this.$store.state.moduleA.count          // 访问state
this.$store.getters.doubleCount          // 访问getters
this.$store.dispatch('incrementAction')  // 提交action
this.$store.commit('increment')          // 提交mutation

这样使得多个模块能够对同一个 gettersmutationaction 做出响应。如果多个 module 有相同名字的 gettermutationaction,就会依次触发,这样可能会出现不是我们想要的结果。

如果希望模块具有更高的封装度和独立性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 gettersactionmutation 都会自动根据模块注册的路径调整命名,如示例代码 6-7-2 所示。

示例代码 6-7-2 modules 的命名空间
const moduleA = {
    namespaced: true,
    state: {
        count: 3,
    },
    mutations: {
        increment(state) {
            console.log('moduleA')
            state.count++
        }
    },
    getters: {
        doubleCount(state) {
            return state.count * 2
        }
    },
    actions: {
        incrementAction (context) {
            context.commit('increment')
        }
    }
}
const moduleB = {
    namespaced: true,
    state: {
        count: 3,
    },
    mutations: {
        increment(state) {
            console.log('moduleB')
            state.count++
        }
    },
    getters: {
        doubleCount(state) {
            return state.count * 2
        }
    },
    actions: {
        incrementAction (context) {
            context.commit('increment')
        }
    }
}

在上面的代码段中定义了两个带有命名空间的 module,然后将它们集成到之前的计数器组件中,如示例代码 6-7-3 所示。

const counter = {
    template: '<div>{{ count }}<button @click="clickCallback">增加</button></div>',
    computed: {
        count() {
            return this.$store.state.moduleA.count // 通过 this.$store.state.moduleA 可以获取 state
        }
    },
    methods: {
        clickCallback() {
            // 通过 this.$store.dispatch 调用 'moduleA/incrementAction' 指定的 action
            this.$store.dispatch('moduleA/incrementAction')
        }
    }
}

const store = Vuex.createStore({
    modules: {
        moduleA: moduleA,
        moduleB: moduleB
    }
})

要调用一个 module 内部的 action 时,需要使用如下代码:

this.$store.dispatch('moduleA/incrementAction')

dispatch 方法参数由 “空间key+'/'+action名” 组成,除了调用指定命名空间的 action 外,当然也可以调用指定命名空间的 mutations,或者存取指定命名空间下的 getters,代码如下:

this.$store.commit('moduleA/increment')
this.$store.getters['moduleA/increment']

若要两个 module 之间进行交互调用,例如把 moduleA 的操作 actionmutation 通知到 moduleBactionmutation 中,那么将 {root: true} 作为第三个参数传给 dispatchcommit 即可。代码如下:

...
const moduleB = {
    namespaced: true,
    actions: {
        incrementAction (context) {
            // 在 moduleB 中提交 moduleA 相关的 mutation
            context.commit('moduleA/increment',null, {root:true})
            // or
            // 在 moduleB 中提交 moduleA 相关的 action
            context.dispatch('moduleA/incrementAction',null, {root:true})
        }
    }
}
...

第一个参数必须由 “空间key+'/'+action名(mutation名)” 组成,这样 Vuex 才可以找到对应命名空间下的 action 或者 mutation。第二个参数是自定义传递的数据,默认为空。第三个参数是 { root: true }

如果需要在 moduleA 内部的 gettersaction 中存取全局的 stategetters,可以利用 rootStaterootGetter 作为第三个和第四个参数传入 getters,同时也会通过 context 对象的属性传入 action,如示例代码 6-7-4 所示。

示例代码 6-7-4 rootState 和 rootGetter 参数的使用
const moduleA = {
    namespaced: true,
    state: {
        count: 3,
    },
    getters: {
        doubleCount(state,getters,rootState,rootGetters) {
            console.log(getters)           // 当前module的getters
            console.log(rootState)         // 全局的state->rootCount: 3
            console.log(rootGetters)       // 全局的getters->rootDoubleCount
            return state.count * 2
        }
    },
    actions: {
        incrementAction (context) {
            console.log(context.rootState)           // 全局的state->rootCount: 3
            console.log(context.rootGetters)         // 全局的getters->rootDoubleCount
        }
    }
}
const store = Vuex.createStore({
    state:{
        rootCount: 3
    },
    getters:{
        rootDoubleCount(state) {
            return state.rootCount * 2
        }
    },
    modules: {
        moduleA: moduleA,
    }
})

若需要在带命名空间的模块注册全局 action(虽然这种应用场景较少遇到),则可添加 root:true,将这个 action 的定义放在函数 handler 中。代码如下:

...
{
    actions: {
        someOtherAction(context) {
            context.dispatch('someAction')
        }
    },
    modules: {
        moduleC: {
            namespaced: true,
                actions: {
                someAction: {
                    root: true,
                    handler(namespace,params) { ... } // -> 'someAction'
                }
            }
        }
    }
}
...

可以看到 Vuexmodule 机制非常灵活,不仅可以在各自的 module 之间相互调用,也可以在全局的 store 中相互调用。这种机制有助于处理复杂项目的状态管理,将单个 store 进行 “组件化”,体现了拆分和分治的原则,这种思想可以借鉴到开发大型项目的架构中,保证代码的稳定性和可维护性,从而提升开发效率。