返回 导航

Vue.js

hangge.com

Vue.js - 使用Vuex进行状态管理详解6(modules模块化)

作者:hangge | 2026-10-08 08:39
    Vuex 通过"单一状态树"来管理应用层级的全部状态。然而,当 store 中的状态数据变得越来越多时,会难以维护和管理。为了解决这个问题,我们可以使用 modules 将状态数据模块化。本文我将详细介绍 modules 的基本使用、局部状态、命名空间、辅助函数以及动态注册等内容。

一、基本介绍

1,为什么需要模块化?

(1)在使用 Vuex 的单一状态树模式下,应用的所有状态都集中在一个大的对象中。随着应用规模的增长,会出现以下问题:
  • store 文件变得臃肿:所有状态、mutations、actions、getters 都写在一个文件中
  • 难以维护:代码量大,逻辑复杂,难以定位问题
  • 协作困难:多人开发时容易产生代码冲突

(2)modules 提供的解决方案:
  • 将 store 分割成多个模块(module)
  • 每个模块拥有自己的 state、mutation、action、getter
  • 支持嵌套子模块,形成树形结构

2,模块的基本结构

一个典型的模块结构如下所示:
提示:模块的 state 使用函数形式(state: () => ({ ... }))定义,这与 Vue 组件的 data 选项类似,可以避免状态被多个实例共享。
// 模块 A
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

// 模块 B
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

// 根模块
const store = createStore({
  modules: {
    a: moduleA,  // 模块 A 被注册为 a
    b: moduleB   // 模块 B 被注册为 b
  }
})

// 访问模块的状态
store.state.a  // -> moduleA 的状态
store.state.b  // -> moduleB 的状态

二、modules 的基本使用

1,创建子模块

(1)下面我们为 Vuex 的 store 增加 home 和 user 两个子模块。首先在 src/store 文件夹下新建 modules 文件夹,然后在其中新建 home.js 和 user.js 文件。

(2)home.js 子模块代码:
const homeModule = {
  state() {
    // home 模块的 state
    return {
      // 在 home 模块中定义一个 homeCounter 全局变量
      homeCounter: 100
    }
  },
  getters: {
    // getters 定义...
  },
  mutations: {
    // mutations 定义...
  },
  actions: {
    // actions 定义...
  }
}

export default homeModule

(3)user.js 子模块代码:
const userModule = {
  state() {
    // user 模块的 state
    return {
      // 在 user 模块中定义一个 userCounter 全局变量
      userCounter: 1000
    }
  },
  getters: {
    // getters 定义...
  },
  mutations: {
    // mutations 定义...
  },
  actions: {
    // actions 定义...
  }
}

export default userModule

2,注册子模块

修改 src/store/index.js 文件,将 home 和 user 两个子模块引入根模块:
提示:modules 对象中的 key 会成为模块的名称,存储到子模块中的数据依然可以实现全局共享。
import { createStore } from 'vuex'
import home from './modules/home'
import user from './modules/user'

const store = createStore({
  state() {
    // 根模块 state
    return {
      counter: 0, // 根模块的变量
      discount: 0.9, // 书籍打9折
    }
  },
  mutations: {
    // 定义 increment 函数,可获取 state 对象
    increment(state) {
      state.counter++
    },
  },
  getters: {
    // 获取当前的折扣
    currentDiscount(state) {
      return state.discount
    },
  },
  actions: {
    // 根模块 actions...
  },
  // 引入 home 和 user 两个子模块
  modules: {
    home: home, // key 指定模块的名称,value 指定引入的模块
    user, // ES6 简写语法,相当于 user: user
  },
})

export default store

3,在组件中访问模块状态

(1)创建一个组件,分别访问根模块和子模块的状态,具体代码如下:
  • 通过 $store.state 获取根模块的 state 对象
  • 通过 $store.state.home 获取 home 子模块的 state 对象
  • 通过 $store.state.user 获取 user 子模块的 state 对象
<template>
  <div>
    <h4>root state 根模块的状态: {{ $store.state.counter }}</h4>
    <h4>home state 子模块的状态: {{ $store.state.home.homeCounter }}</h4>
    <h4>user state 子模块的状态: {{ $store.state.user.userCounter }}</h4>
  </div>
</template>

<script>
export default {}
</script>

(2)运行效果:

三、modules 的局部状态

1,局部状态的概念

子模块定义的 state 属于子模块的状态,称为局部状态。以下是子模块的一些特点:
  • 在子模块内部,mutation 和 getter 函数接收的第一个参数 state 是局部状态
  • 在子模块内部,对于 action 函数,局部状态可以通过 context.state 暴露出来,根节点状态则通过 context.rootState 暴露出来
  • 在子模块内部,getter 函数的根节点状态(rootState)会作为第三个参数暴露出来

2,局部状态的使用示例

(1)下面修改 user.js 子模块,展示局部状态的使用方式:
注意:在子模块内不要直接修改 rootState,应该通过提交 mutation 来更新状态。
const userModule = {
  state() {
    // state 属于 user 模块的状态,称为局部状态
    return {
      userCounter: 1000
    }
  },
  getters: {
    // user 模块的 getter 函数接收的第一个参数是局部状态 (state)
    doubleUserCount(state) {
      return state.userCounter * 2
    },
    // user 模块的 getter 函数,根节点状态 (rootState) 作为第三个参数暴露出来
    userCountAddRootCount(state, getters, rootState) {
      return state.userCounter + rootState.counter
    }
  },
  mutations: {
    increment(state) {
      // state 是 user 模块的局部状态
      state.userCounter++
    }
  },
  actions: {
    // 在 action 函数中,局部状态通过 context.state 暴露出来
    // 根节点状态通过 context.rootState 暴露出来
    incrementAction({ state, commit, rootState }) {
      commit('increment')
    }
  }
}

export default userModule

(2)创建一个组件来展示 user 子模块的局部状态:
<template>
  <div>
    <h4>root state 根模块的状态: {{ $store.state.counter }}</h4>
    <h4>root state 根模块 currentDiscount: {{ $store.getters.currentDiscount }}</h4>
    <!-- 访问子模块的 state 和 getters -->
    <h4>user state 子模块 userCounter 的状态: {{ $store.state.user.userCounter }}</h4>
    <h4>user state 子模块 doubleUserCount: {{ $store.getters.doubleUserCount }}</h4>
    <h4>user state 子模块 userCountAddRootCount: {{ $store.getters.userCountAddRootCount }}</h4>
    <button @click="incrementAction">+1</button>
  </div>
</template>

<script>
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    const incrementAction = () => {
      // 会触发 root 模块和 user 子模块的 incrementAction 函数
      store.dispatch('incrementAction')
    }

    return { incrementAction }
  }
}
</script>

(3)运行效果如下,当点击"+1"按钮时,会分发 incrementAction,这会同时触发根模块和 user 子模块中的 incrementAction 函数。这是因为默认情况下,模块内的 action、mutation 和 getter 仍然注册在全局命名空间中。

四、modules 命名空间

1,什么是命名空间?

(1)默认情况下,子模块内部的 action、getter 和 mutation 仍然注册在全局的命名空间中,这使得多个模块可以对同一个 action 或 mutation 作出响应。

(2)为了解决这个问题,可以通过添加 namespaced: true 使其成为带有命名空间的模块:
  • 模块内的 getter、action 和 mutation 都会自动根据模块注册的路径调整命名
  • 使用时需要添加模块名称前缀,如 store.dispatch('home/incrementAction')

2,命名空间的使用样例

(1)修改 home.js 子模块,添加 namespaced: true:
提示:添加 namespaced: true 后,该模块的 getter、action 和 mutation 都会自动添加模块名前缀。
const homeModule = {
  namespaced: true,  // 为 home 模块添加命名空间
  state() {
    return {
      homeCounter: 100
    }
  },
  getters: {
    doubleHomeCount(state) {
      return state.homeCounter * 2
    },
    homeCountAddRootCount(state, getters, rootState) {
      return state.homeCounter + rootState.counter
    }
  },
  mutations: {
    increment(state) {
      state.homeCounter++
    }
  },
  actions: {
    incrementAction({ state, commit, rootState }) {
      commit('increment')
    }
  }
}

export default homeModule

(2)创建一个组件,展示如何访问带命名空间的模块,具体代码如下:
  • 访问 getters 时使用数组语法:$store.getters['home/doubleHomeCount']
  • 分发 action 时需要添加模块前缀:store.dispatch('home/incrementAction')
<template>
  <div>
    <h4>root state 根模块的状态: {{ $store.state.counter }}</h4>
    <!-- 访问子模块的 state 和 getters(需要添加模块前缀)-->
    <h4>home 子模块 homeCounter 的状态: {{ $store.state.home.homeCounter }}</h4>
    <h4>home 子模块 doubleHomeCount: {{ $store.getters['home/doubleHomeCount'] }}</h4>
    <h4>home 子模块 homeCountAddRootCount: {{ $store.getters['home/homeCountAddRootCount'] }}</h4>
    <button @click="incrementAction">+1</button>
  </div>
</template>

<script>
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    const incrementAction = () => {
      // 只会触发 home 模块的 incrementAction 函数
      store.dispatch('home/incrementAction')
    }

    return { incrementAction }
  }
}
</script>

(3)运行效果如下,可用看到现在点击"+1"按钮只会触发 home 子模块的 incrementAction 函数。

五、带命名空间的模块访问全局内容

1,访问根模块的 state 和 getters

如果希望在子模块中使用根 state 和 getters,可以通过以下方式:
  • getter 函数:第三个和第四个参数分别是 rootState 和 rootGetters
  • action 函数:通过 context 对象的 rootState 和 rootGetters 属性访问

2,访问全局的 action 和 mutation

在子模块中,如果想要分发全局命名空间内的 action 或提交 mutation,只需将 { root: true } 作为第三个参数传递:
提示:在带命名空间的模块内,dispatch 和 commit 默认作用于当前模块。添加 { root: true } 可以访问根模块。
const homeModule = {
  namespaced: true,
  state() {
    return {
      homeCounter: 100
    }
  },
  getters: {
    // home 模块: state, getters
    // 根模块: rootState, rootGetters
    homeCountAddRootCount(state, getters, rootState, rootGetters) {
      return state.homeCounter + rootState.counter
    }
  },
  actions: {
    // home 模块: state, commit, dispatch, getters
    // 根模块: rootState, rootGetters
    incrementAction({ state, commit, dispatch, getters, rootState, rootGetters }) {
      commit('increment')                      // 提交到当前模块的 mutation
      commit('increment', null, { root: true }) // 提交到根模块的 mutation
      dispatch('incrementAction', null, { root: true }) // 分发到根模块的 action
    }
  }
}

export default homeModule

3,注册全局 action

若需要在带命名空间的模块中注册全局 action,可以添加 root: true,并将这个 action 的定义放在 handler 函数中:
const homeModule = {
  namespaced: true,
  actions: {
    // 这是一个全局 action,不带模块前缀
    someAction: {
      root: true,
      handler(namespacedContext, payload) {
        // someAction 会被注册到全局命名空间
      }
    }
  }
}

六、modules 辅助函数

1,方式一:映射时指定模块名前缀

在使用带命名空间的模块时,可以通过在映射时直接添加模块名前缀来简化代码:
<template>
  <div>
    <h4>home 子模块 homeCounter 的状态: {{ homeCounter }}</h4>
    <h4>home 子模块 doubleHomeCount: {{ doubleHomeCount }}</h4>
    <button @click="homeIncrementCommit">+1</button>
    <button @click="incrementAction">+1</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    // 方式一:mapState 映射
    ...mapState({
      homeCounter: state => state.home.homeCounter
    }),
    // 方式一:mapGetters 映射
    ...mapGetters({
      doubleHomeCount: 'home/doubleHomeCount'
    })
  },
  methods: {
    // 方式一:mapMutations 映射
    ...mapMutations({
      homeIncrementCommit: 'home/increment'
    }),
    // 方式一:mapActions 映射
    ...mapActions({
      incrementAction: 'home/incrementAction'
    })
  }
}
</script>

2,方式二:辅助函数第一个参数作为模块名前缀

可以将模块名前缀作为辅助函数的第一个参数:
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    // 方式二:将模块名作为第一个参数
    ...mapState('home', ['homeCounter']),
    ...mapGetters('home', ['doubleHomeCount'])
  },
  methods: {
    // 方式二:将模块名作为第一个参数
    ...mapMutations('home', {
      homeIncrementCommit: 'increment'
    }),
    ...mapActions('home', ['incrementAction'])
  }
}
</script>

3,方式三:使用 createNamespacedHelpers(推荐)

Vuex 提供了 createNamespacedHelpers 函数,可以创建基于某个命名空间的辅助函数:
提示:这是最推荐的方式,代码简洁且易于维护。创建的辅助函数自动绑定到指定的命名空间。
<script>
import { createNamespacedHelpers } from 'vuex'

// 方式三:使用 createNamespacedHelpers 创建绑定到 home 模块的辅助函数
const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers('home')

export default {
  computed: {
    // 不需要再写模块前缀
    ...mapState(['homeCounter']),
    ...mapGetters(['doubleHomeCount'])
  },
  methods: {
    ...mapMutations({
      homeIncrementCommit: 'increment'
    }),
    ...mapActions(['incrementAction'])
  }
}
</script>

4,方式四:在 setup 中使用(Composition API)

(1)在 Composition API 的 setup 中,可以结合 createNamespacedHelpers 和自定义 Hook 使用:
<script>
import { computed } from 'vue'
import { createNamespacedHelpers, useStore } from 'vuex'

// 创建绑定到 home 模块的辅助函数
const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers('home')

export default {
  setup() {
    const store = useStore()

    // 手动创建计算属性
    const homeCounter = computed(() => store.state.home.homeCounter)
    const doubleHomeCount = computed(() => store.getters['home/doubleHomeCount'])

    // 获取 mutations 和 actions
    const mutationFuncs = mapMutations({
      homeIncrementCommit: 'increment'
    })
    const actionsFuncs = mapActions(['incrementAction'])

    return {
      homeCounter,
      doubleHomeCount,
      ...mutationFuncs,
      ...actionsFuncs
    }
  }
}
</script>

(2)为了简化 mapState 和 mapGetters 在 setup 中的使用,可以封装一个 useMapper 工具函数:
// src/hooks/useMapper.js
import { computed } from 'vue'
import { useStore } from 'vuex'

export function useMapper(mapFn, mapper) {
  // 1. 获取 store 对象
  const store = useStore()

  // 2. 获取映射的结果
  const storeStateFns = mapFn(mapper)

  // 3. 将普通函数转换为计算属性函数
  const storeState = {}
  Object.keys(storeStateFns).forEach(fnKey => {
    const fn = storeStateFns[fnKey].bind({ $store: store })
    storeState[fnKey] = computed(fn)
  })

  return storeState
}

(3)使用封装好的 useMapper 函数:
<script>
import { createNamespacedHelpers } from 'vuex'
import { useMapper } from '../hooks/useMapper'

const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers('home')

export default {
  setup() {
    // 方式四:使用自定义 Hook
    const stateFunc = useMapper(mapState, ['homeCounter'])
    const gettersFunc = useMapper(mapGetters, ['doubleHomeCount'])
    const mutationFuncs = mapMutations({ homeIncrementCommit: 'increment' })
    const actionsFuncs = mapActions(['incrementAction'])

    return {
      ...stateFunc,
      ...gettersFunc,
      ...mutationFuncs,
      ...actionsFuncs
    }
  }
}
</script>

附一:模块动态注册

1,动态注册模块

(1)在 store 创建之后,可以使用 store.registerModule 方法动态注册模块:
import { createStore } from 'vuex'

const store = createStore({ /* 选项 */ })

// 注册模块 `myModule`
store.registerModule('myModule', {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
})

// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

(2)注册后可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态。
提示:模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。

2,动态卸载模块

使用 store.unregisterModule(moduleName) 方法动态卸载模块:
// 卸载动态注册的模块
store.unregisterModule('myModule')
注意:不能使用此方法卸载静态模块(即创建 store 时声明的模块)。

3,检查模块是否已注册

使用 store.hasModule(moduleName) 方法检查模块是否已被注册:
// 检查模块是否已注册
if (store.hasModule('myModule')) {
  console.log('myModule 已注册')
}

// 嵌套模块需要使用数组形式
if (store.hasModule(['nested', 'myModule'])) {
  console.log('nested/myModule 已注册')
}

4,保留 state

在注册新模块时,如果想保留过去的状态(例如从服务端渲染的应用保留 state),可以使用 preserveState 选项:
store.registerModule('a', module, { preserveState: true })

附二:最佳实践

1,模块划分原则

(1)按照功能或业务领域划分模块,例如:
  • user:用户相关状态(登录信息、权限等)
  • products:商品相关状态
  • cart:购物车相关状态
  • app:应用全局状态(主题、语言等)
(2)推荐的项目目录结构:
src/
└── store/
    ├── index.js          # 主 store 文件
    └── modules/
        ├── user.js       # 用户模块
        ├── products.js   # 商品模块
        ├── cart.js       # 购物车模块
        └── app.js        # 应用全局模块

2,始终使用命名空间

(1)建议为所有模块添加 namespaced: true:
const myModule = {
  namespaced: true,  // 始终添加命名空间
  state: () => ({ ... }),
  // ...
}
(2)好处:
  • 避免不同模块之间的命名冲突
  • 提高模块的封装性和可复用性
  • 代码更清晰,易于维护

3,使用常量定义类型

在大型项目中,建议使用常量代替 mutation 和 action 类型字符串:
// mutation-types.js
export const USER_SET_INFO = 'user/SET_INFO'
export const USER_LOGOUT = 'user/LOGOUT'

// user.js
import { USER_SET_INFO, USER_LOGOUT } from '../mutation-types'

const userModule = {
  namespaced: true,
  mutations: {
    [USER_SET_INFO](state, payload) {
      // ...
    }
  }
}
评论

全部评论(0)

回到顶部