modules
由于使用单个状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将 store 分割成模块(Module)。每个模块都拥有自己的 state、mutations、actions、getters,甚至是嵌套子模块。最后在根 store 采用 modules 这个设置项将各个模块汇集进来,如示例代码 6-7-1 所示。
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')
}
}
}
...
在默认情况下,模块内部的 action、mutation 和 getters 注册在全局命名空间中,可以不受 module 限制,而 state 在 module 内部,它们可以通过下面这种方式获取到:
this.$store.state.moduleA.count // 访问state
this.$store.getters.doubleCount // 访问getters
this.$store.dispatch('incrementAction') // 提交action
this.$store.commit('increment') // 提交mutation
这样使得多个模块能够对同一个 getters、mutation 或 action 做出响应。如果多个 module 有相同名字的 getter、mutation 或 action,就会依次触发,这样可能会出现不是我们想要的结果。
如果希望模块具有更高的封装度和独立性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getters、action 及 mutation 都会自动根据模块注册的路径调整命名,如示例代码 6-7-2 所示。
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 的操作 action 或 mutation 通知到 moduleB 的 action 或 mutation 中,那么将 {root: true} 作为第三个参数传给 dispatch 或 commit 即可。代码如下:
...
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 内部的 getters 或 action 中存取全局的 state 或 getters,可以利用 rootState 和 rootGetter 作为第三个和第四个参数传入 getters,同时也会通过 context 对象的属性传入 action,如示例代码 6-7-4 所示。
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'
}
}
}
}
}
...
可以看到 Vuex 的 module 机制非常灵活,不仅可以在各自的 module 之间相互调用,也可以在全局的 store 中相互调用。这种机制有助于处理复杂项目的状态管理,将单个 store 进行 “组件化”,体现了拆分和分治的原则,这种思想可以借鉴到开发大型项目的架构中,保证代码的稳定性和可维护性,从而提升开发效率。