由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成 模块(module).每个模块拥有自己的 state、mutaion、action、getter、甚至是嵌套子模块—— 从上至下进行同样方式的分割:
const moduleA = {
state:()=>({...}),
mutations:{...},
getters:{...},
actions:{...}
}
const moduleB = {
state:() => ({...}),
mutations:{...},
actions:{...}
}
const store = new Vuex.Store({
modules:{
a:moudleB,
b:moudleA
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数 是 模块的局部状态对象。
const moduleA = {
state:() => ({
count:0
}),
mutations:{
increment(state){
// 这里的'state' 对象是模块的局部状态
state.count++
}
},
getters:{
doubleCount(state){
return state.count * 2
}
}
}
同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:
const moduleA = {
// ...
actions:{
incrementIfOddOnRootSum({state,commit,rootState}){
if((state.count + rootState.count)%2===1){
commit('increment')
}
}
}
}
对于模块内部的getter,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters:{
sumWithRootCount(state,getters,rootState){
return state.count + rootState.count
}
}
}
命名空间
默认情况下,模块内部的action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 做出响应。
如果希望你的模块具有更高的封装都和复用性,你可以通过添加 namespaced:true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:
const store = new Vuex.Store({
modules:{
account:{
namespaced:true,
// 模块内容 (module assets)
state:() => ({ ... }), //模块内的状态已经是嵌套了,使用'namespaced' 属性不会对其产生影响
getters:{
isAdmin(){ ... } // -> getters['account/is']
},
actions:{
login(){ ... } // -> dispatch('account/login')
},
mutations:{
login(){ ... } // -> commit('account/login')
},
// 嵌套模块
modules:{
// 继承父模块的命名空间
myPage:{
state:() => ({ ... }),
getters:{
profile(){ ... } // -> getters['account/profile']
},
// 进一步嵌套命名空间
posts:{
namespaced:true,
state:() => ({ ... }),
getters:{
popular(){} // -> getters['account/posts/popular']
}
}
}
}
}
}
})
启用了命名空间的 getter 和 action 会收到局部化的 getter,dispatch 和 commit。换言之,你在使用模块内容( module assets )时不需要在同意模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。
在带命名空间的模块内访问全局内容 ( Global assets )
如果你希望使用全局 state 和 getter rootState 和 rootGetters 会作为第三个和第四参数传入getter,也会通过 context 对象的属性传入 action。
若需要在全局命名空间内分发 action 或 提交mutation,将 {root:true} 作为第三参数传给 dispatch 或 commit即可。
modules:{
foo:{
namespaced:true,
getters:{
// 在这个模块的getter中,'getters' 被局部化了
// 你可以使用 getter 的第四个参数来调用 'rootGetters'
someGetter(state,getters,rootState,rootGetters){
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter:state => {...}
},
actions:{
// 在这个模块中,dispatch 和 commit 也被局部化了
// 他们可以接收 'root' 属性以访问根 dispatch 或 commit
someAction({ dispatch ,commit,getters,rootGetters}){
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction',null,{ root:true}) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation',null,{root:true}) // -> 'someMutation'
}
someOtherAction(ctx,payload){...}
}
}
}
在带命名空间的模块注册全局 action
若需要在带命名空间的模块注册全局 action,你可添加 root:true,并将这个 action 的定义放在函数 handler中。例如:
{
actions:{
someOtherAction({dispatch}){
dispatch('someAction')
}
},
modules:{
foo:{
namespaced:true,
actions:{
someAction:{
root:true,
handler(namespacedContext,payload){ ... } // -> 'someAction'
}
}
}
}
}
带命名空间的绑定函数
当使用 mapState,mapGetters,mapActions 和 mapMutation 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐
computed:{
...mapState({
a:state=>state.some.nested.module.a,
b:state=>state.some.nested.module.b
})
}
methods:{
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar', // -> this['some/nested/module/bar']()
])
}
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:
computed:{
...mapState('some/nested/module',{
a:state=>state.a,
b:state=>state.b
})
},
methods:{
...mapActions('some/nested.module',[
'foo' // -> this.foo()
'bar' // -> this.bar()
])
}
而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间函数。
它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数
import { createNamespacedHelpers } from 'vuex'
const { mapState,mapActions} = createNamespacedHelper('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'
])
}
}
给插件开发者的注意事项
如果你开发的 插件(plugin)提供了模块并允许用户将其添加到 Vuex store,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称:
// 通过插件的参数对象得到空间名称
// 然后返回Vuex 插件函数
export function createPlugin(options = {}){
return function(store){
// 把空间名字添加到插件模块的类型( type )中去
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
}
}
模块动态注册
在 store 创建 之后,你可以使用 store.registerMOdule 方法注册模块:
import Vuex from 'vuex'
const store = new Vuex.Store({ /* 选项*/ })
//注册模块 'myModule'
store.registerModule('myModule',{
// ...
})
//注册嵌套模块 'nested/moModule'
store.registerModule([nested,myModule],{
// ...
})
之后就可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态。
模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如:vuex-router-sync 插件就是通过动态注册模块将 vue-router 和 vuex 结合在一起,实现应用的路由状态管理。
你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store时声明的模块)。
注意,你可以通过 store.hasModule(moduleName) 方法检查该模块是否已经被注册到了 store
保留 state
在注册一个新module时,你很有可能想保留过去的 state,例如 从一个服务端渲染的应用保留state。你可以通过 preserveState 选项将其归档:
store.registerModule(‘a’,module,{preserveState:true}).
当你设置 preserveState:true **时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是state 不会。这里假设 store 的 state 已经包含了这个module 的state 并且你不希望将其覆写
模块重用
有时我们可能需要创建一个模块的多个实例,例如:
- 创建多个store,他们公用同一个模块(例如:当 runInNewContext 选项是’false’ 或 ‘once‘ )时,为了在 服务端渲染中避免有状态的单例
- 在一个store 中多次注册同一个模块
如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。
实际上这个 Vue 组件内的 data 是同样的问题。因此解决方法也是相同的——使用一个函数来声明模块状态(2.3.0 + 支持):
const myReusableModule = {
state:()=>({
foo:'bar'
}),
// mutation,action 个 getter 等等...
}