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
进行 “组件化”,体现了拆分和分治的原则,这种思想可以借鉴到开发大型项目的架构中,保证代码的稳定性和可维护性,从而提升开发效率。