模块

由于使用单个状态树,我们应用程序的所有状态都包含在一个大型对象中。但是,随着应用程序规模的增长,存储可能会变得非常臃肿。

为了解决这个问题,Vuex 允许我们将存储划分为 **模块**。每个模块都可以包含自己的状态、Mutation、Action、Getter,甚至嵌套模块——它一直都是分形的。

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

模块局部状态

在模块的 Mutation 和 Getter 中,接收到的第一个参数将是 **模块的局部状态**。

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // `state` is the local module 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 仍然在 **全局命名空间** 下注册——这允许多个模块对相同的 Action/Mutation 类型做出反应。Getter 默认情况下也在全局命名空间中注册。但是,目前这没有功能性目的(为了避免破坏性更改)。您必须小心,不要在不同的非命名空间模块中定义两个具有相同名称的 Getter,这会导致错误。

如果您希望模块更加独立或可重用,可以使用 namespaced: true 将其标记为命名空间。当模块注册时,它的所有 Getter、Action 和 Mutation 将根据模块注册的路径自动进行命名空间化。例如

const store = createStore({
  modules: {
    account: {
      namespaced: true,

      // module assets
      state: () => ({ ... }), // module state is already nested and not affected by namespace option
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // nested modules
      modules: {
        // inherits the namespace from parent module
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // further nest the namespace
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

命名空间化的 Getter 和 Action 将接收本地化的 gettersdispatchcommit。换句话说,您可以在同一个模块中使用模块资源,而无需编写前缀。在命名空间之间切换不会影响模块内部的代码。

在命名空间模块中访问全局资源

如果您想使用全局状态和 Getter,rootStaterootGetters 将作为第三个和第四个参数传递给 Getter 函数,并且还作为传递给 Action 函数的 context 对象上的属性公开。

要分派全局命名空间中的 Action 或提交 Mutation,将 { root: true } 作为第三个参数传递给 dispatchcommit

modules: {
  foo: {
    namespaced: true,

    getters: {
      // `getters` is localized to this module's getters
      // you can use rootGetters via 4th argument of getters
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
        rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // dispatch and commit are also localized for this module
      // they will accept `root` option for the root dispatch/commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'
        rootGetters['bar/someGetter'] // -> 'bar/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'
        }
      }
    }
  }
}

使用命名空间绑定辅助函数

使用 mapStatemapGettersmapActionsmapMutations 辅助函数将命名空间模块绑定到组件时,可能会变得有点冗长。

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  }),
  ...mapGetters([
    'some/nested/module/someGetter', // -> this['some/nested/module/someGetter']
    'some/nested/module/someOtherGetter', // -> this['some/nested/module/someOtherGetter']
  ])
},
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
  }),
  ...mapGetters('some/nested/module', [
    'someGetter', // -> this.someGetter
    'someOtherGetter', // -> this.someOtherGetter
  ])
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

此外,您可以使用 createNamespacedHelpers 创建命名空间辅助函数。它返回一个对象,该对象包含使用给定命名空间值绑定的新的组件绑定辅助函数。

import { createNamespacedHelpers } from 'vuex'

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

export default {
  computed: {
    // look up in `some/nested/module`
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // look up in `some/nested/module`
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

插件开发者的注意事项

当您创建一个 插件 来提供模块并让用户将它们添加到 Vuex 存储中时,您可能关心模块的不可预测的命名空间。如果插件用户在命名空间模块下添加您的模块,您的模块也将被命名空间化。为了适应这种情况,您可能需要通过插件选项接收命名空间值。

// get namespace value via plugin option
// and returns Vuex plugin function
export function createPlugin (options = {}) {
  return function (store) {
    // add namespace to plugin module's types
    const namespace = options.namespace || ''
    store.dispatch(namespace + 'pluginAction')
  }
}

动态模块注册

您可以在创建存储 **之后** 使用 store.registerModule 方法注册模块。

import { createStore } from 'vuex'

const store = createStore({ /* options */ })

// register a module `myModule`
store.registerModule('myModule', {
  // ...
})

// register a nested module `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

模块的状态将作为 store.state.myModulestore.state.nested.myModule 公开。

动态模块注册使其他 Vue 插件也可以通过将模块附加到应用程序的存储来利用 Vuex 进行状态管理。例如,vuex-router-sync 库通过在动态附加的模块中管理应用程序的路由状态,将 vue-router 与 vuex 集成。

您还可以使用 store.unregisterModule(moduleName) 删除动态注册的模块。请注意,您无法使用此方法删除静态模块(在创建存储时声明)。

请注意,您可以通过 store.hasModule(moduleName) 方法检查模块是否已注册到存储中。需要注意的是,嵌套模块应作为数组传递给 registerModulehasModule,而不是作为指向模块路径的字符串。

保留状态

您可能希望在注册新模块时保留以前的状态,例如保留来自服务器端渲染应用程序的状态。您可以使用 preserveState 选项实现这一点:store.registerModule('a', module, { preserveState: true })

当您设置 preserveState: true 时,模块将被注册,Action、Mutation 和 Getter 将被添加到存储中,但状态不会被添加。假设您的存储状态已经包含该模块的状态,并且您不想覆盖它。

模块重用

有时我们可能需要创建模块的多个实例,例如

  • 创建使用相同模块的多个存储(例如,当 runInNewContext 选项为 false'once' 时,避免 SSR 中有状态的单例);
  • 在同一个存储中多次注册同一个模块。

如果我们使用普通对象来声明模块的状态,那么该状态对象将被引用共享,并且当它被修改时会导致跨存储/模块状态污染。

这实际上与 Vue 组件中的 data 存在完全相同的问题。因此,解决方案也是一样的——使用函数来声明模块状态(在 2.3.0+ 中支持)。

const MyReusableModule = {
  state: () => ({
    foo: 'bar'
  }),
  // mutations, actions, getters...
}