{T}

模块化

当应用变得复杂时,使用模块化可以将 store 分割成模块,每个模块拥有自己的 state、mutation、action、getter 和嵌套子模块。

概述

Vuex 使用单一状态树,当应用变得非常复杂时,store 对象可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将 store 分割成模块(Module)

模块化解决的问题

  • ✅ 状态管理结构清晰
  • ✅ 便于团队协作开发
  • ✅ 模块可复用
  • ✅ 便于测试和维护

模块结构示意

code
Store (根模块)
├── state
├── getters
├── mutations
├── actions
└── modules
    ├── user (用户模块)
    │   ├── state
    │   ├── getters
    │   ├── mutations
    │   ├── actions
    │   └── modules
    │       └── profile
    ├── cart (购物车模块)
    │   ├── state
    │   ├── getters
    │   ├── mutations
    │   └── actions
    └── products (商品模块)
        ├── state
        ├── getters
        ├── mutations
        └── actions

基本用法

定义模块

javascript
// modules/user.js
const userModule = {
  state: {
    userInfo: null,
    token: null
  },
  mutations: {
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo
    },
    setToken(state, token) {
      state.token = token
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const response = await api.login(credentials)
      commit('setToken', response.token)
      commit('setUserInfo', response.user)
    }
  },
  getters: {
    isLoggedIn: state => !!state.token,
    userName: state => state.userInfo?.name || ''
  }
}

export default userModule

注册模块

javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
import products from './modules/products'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    globalLoading: false
  },
  modules: {
    user,
    cart,
    products
  }
})

export default store

在组件中使用模块状态

Vue SFC
<template>
  <div>
    <p>用户名: {{ userName }}</p>
    <p>购物车数量: {{ cartCount }}</p>
  </div>
</template>

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

export default {
  computed: {
    // 访问模块的 state
    ...mapState({
      userInfo: state => state.user.userInfo,
      cartItems: state => state.cart.items
    }),
    
    // 访问模块的 getters
    ...mapGetters([
      'isLoggedIn',    // user 模块
      'cartCount',     // cart 模块
      'productList'    // products 模块
    ])
  }
}
</script>

模块的局部状态

模块内部的 state

模块内的 state 是局部的,只能通过 state.模块名 访问。

javascript
const moduleA = {
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      // state 是模块的局部状态
      state.count++
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  }
}

访问根节点状态

在模块内部,可以通过第三个参数访问根节点状态。

javascript
const moduleA = {
  state: {
    count: 0
  },
  getters: {
    // 参数:局部 state, 局部 getters, 根 state
    sumWithRootCount(state, getters, rootState) {
      return state.count + rootState.count
    }
  },
  actions: {
    // context 对象包含 rootState
    incrementIfOddOnRootSum({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

Action 中访问根节点

javascript
const moduleA = {
  actions: {
    async fetchUserAndGlobalData({ state, commit, rootState, dispatch }) {
      // 访问局部状态
      console.log(state.userInfo)
      
      // 访问根状态
      console.log(rootState.globalLoading)
      
      // 调用其他模块的 action
      await dispatch('cart/fetchCartItems', null, { root: true })
      
      // 提交其他模块的 mutation
      commit('setGlobalLoading', true, { root: true })
    }
  }
}

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的,这会导致命名冲突。通过添加 namespaced: true 可以使模块具有更高的封装度和复用性。

启用命名空间

javascript
const user = {
  namespaced: true,  // 启用命名空间
  
  state: {
    userInfo: null
  },
  mutations: {
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo
    }
  },
  actions: {
    async fetchUser({ commit }) {
      const user = await api.getUser()
      commit('setUserInfo', user)
    }
  },
  getters: {
    userName: state => state.userInfo?.name
  }
}

在组件中使用命名空间模块

方式一:完整路径访问

Vue SFC
<script>
export default {
  computed: {
    userInfo() {
      return this.$store.state.user.userInfo
    },
    userName() {
      return this.$store.getters['user/userName']
    }
  },
  methods: {
    fetchUser() {
      this.$store.dispatch('user/fetchUser')
    },
    setUserInfo(userInfo) {
      this.$store.commit('user/setUserInfo', userInfo)
    }
  }
}
</script>

方式二:使用 createNamespacedHelpers

Vue SFC
<template>
  <div>
    <p>用户名: {{ userName }}</p>
    <button @click="fetchUser">获取用户</button>
  </div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'

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

export default {
  computed: {
    ...mapState(['userInfo']),
    ...mapGetters(['userName', 'isLoggedIn'])
  },
  methods: {
    ...mapMutations(['setUserInfo', 'setToken']),
    ...mapActions(['fetchUser', 'login', 'logout'])
  }
}
</script>

方式三:mapXXX 函数指定命名空间

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

export default {
  computed: {
    // 指定命名空间
    ...mapState('user', ['userInfo', 'token']),
    ...mapGetters('user', ['userName', 'isLoggedIn']),
    
    // 也可以使用对象形式
    ...mapState('cart', {
      cartItems: 'items',
      cartCount: 'count'
    })
  },
  methods: {
    ...mapMutations('user', ['setUserInfo']),
    ...mapActions('user', ['fetchUser', 'login'])
  }
}
</script>

命名空间对比

访问方式无命名空间有命名空间
Statestate.user.userInfostate.user.userInfo
Gettergetters.userNamegetters['user/userName']
Mutationcommit('setUserInfo')commit('user/setUserInfo')
Actiondispatch('fetchUser')dispatch('user/fetchUser')

Module 命名空间实现原理

Vuex 通过 ModuleCollectioninstallModule 处理命名空间:

javascript
// src/module/module-collection.js
// 注册模块时构建命名空间路径
class ModuleCollection {
  register(path, rawModule) {
    const newModule = new Module(rawModule)
    if (path.length === 0) {
      this.root = newModule  // 根模块
    } else {
      // 找到父模块并添加到 _children
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }
    // 递归注册子模块
    if (rawModule.modules) {
      Object.keys(rawModule.modules).forEach(key => {
        this.register(path.concat(key), rawModule.modules[key])
      })
    }
  }
}

// 命名空间路径生成:namespaced: true 时
// getNamespace(path) → path.join('/')  → 'cart/products'
// 最终 mapState('cart', ['items']) → 访问 store.state.cart.items

关键:namespaced 模块的 getter/action/mutation 都会加上模块路径前缀(如 cart/),避免不同模块间的方法名冲突。

在命名空间模块内访问全局内容

javascript
const moduleA = {
  namespaced: true,
  
  getters: {
    // 第三个参数是 rootState,第四个参数是 rootGetters
    someGetter(state, getters, rootState, rootGetters) {
      return state.count + rootState.count
    }
  },
  
  actions: {
    // 在 action 中使用 { root: true } 访问全局
    someAction({ dispatch, commit, getters, rootGetters }) {
      // 调用全局 action
      dispatch('someAction', null, { root: true })
      
      // 调用其他模块的 action
      dispatch('moduleB/someAction', null, { root: true })
      
      // 提交全局 mutation
      commit('someMutation', null, { root: true })
    }
  }
}

模块注册

静态注册

在创建 store 时注册模块:

javascript
const store = new Vuex.Store({
  modules: {
    user: userModule,
    cart: cartModule
  }
})

动态注册

在 store 创建后注册模块:

javascript
// 注册模块
store.registerModule('cart', {
  state: {
    items: []
  },
  mutations: {
    addItem(state, item) {
      state.items.push(item)
    }
  }
})

// 注册嵌套模块
store.registerModule(['user', 'profile'], profileModule)

// 检查模块是否已注册
if (store.hasModule('cart')) {
  console.log('cart 模块已注册')
}

// 卸载模块
store.unregisterModule('cart')

动态注册的实际应用

javascript
// 按需加载模块
const loadModule = async (moduleName) => {
  if (store.hasModule(moduleName)) {
    return
  }
  
  const module = await import(`./modules/${moduleName}`)
  store.registerModule(moduleName, module.default)
}

// 在路由守卫中加载模块
router.beforeEach(async (to, from, next) => {
  if (to.meta.requiresModule) {
    await loadModule(to.meta.moduleName)
  }
  next()
})

保留状态注册模块

javascript
// 注册模块时保留之前的状态
store.registerModule('cart', cartModule, { preserveState: true })

模块热重载

javascript
// store/index.js
if (module.hot) {
  module.hot.accept(['./modules/user', './modules/cart'], () => {
    const newUser = require('./modules/user').default
    const newCart = require('./modules/cart').default
    
    store.hotUpdate({
      modules: {
        user: newUser,
        cart: newCart
      }
    })
  })
}

模块重用

创建可重用的模块

javascript
// 创建一个可重用的模块工厂函数
function createCounterModule(initialState = { count: 0 }) {
  return {
    namespaced: true,
    state: () => ({ ...initialState }),
    mutations: {
      increment(state) {
        state.count++
      },
      decrement(state) {
        state.count--
      },
      reset(state) {
        state.count = initialState.count
      }
    },
    getters: {
      doubleCount: state => state.count * 2
    }
  }
}

// 使用工厂函数创建多个实例
const store = new Vuex.Store({
  modules: {
    counterA: createCounterModule({ count: 10 }),
    counterB: createCounterModule({ count: 20 })
  }
})

模块模板

javascript
// 模块模板
const createEntityModule = (entityName, api) => ({
  namespaced: true,
  
  state: () => ({
    items: [],
    current: null,
    loading: false,
    error: null
  }),
  
  mutations: {
    setItems(state, items) {
      state.items = items
    },
    setCurrent(state, item) {
      state.current = item
    },
    setLoading(state, loading) {
      state.loading = loading
    },
    setError(state, error) {
      state.error = error
    }
  },
  
  actions: {
    async fetchAll({ commit }) {
      commit('setLoading', true)
      try {
        const items = await api.fetchAll()
        commit('setItems', items)
      } catch (error) {
        commit('setError', error.message)
      } finally {
        commit('setLoading', false)
      }
    },
    
    async fetchOne({ commit }, id) {
      commit('setLoading', true)
      try {
        const item = await api.fetchOne(id)
        commit('setCurrent', item)
      } catch (error) {
        commit('setError', error.message)
      } finally {
        commit('setLoading', false)
      }
    }
  },
  
  getters: {
    itemCount: state => state.items.length,
    isLoading: state => state.loading
  }
})

// 使用模板创建模块
const userModule = createEntityModule('user', userApi)
const productModule = createEntityModule('product', productApi)

完整示例

用户模块

javascript
// store/modules/user.js
import { SET_USER, SET_TOKEN, CLEAR_USER } from '../mutation-types'

const state = {
  userInfo: null,
  token: localStorage.getItem('token') || null
}

const mutations = {
  [SET_USER](state, user) {
    state.userInfo = user
  },
  
  [SET_TOKEN](state, token) {
    state.token = token
    if (token) {
      localStorage.setItem('token', token)
    } else {
      localStorage.removeItem('token')
    }
  },
  
  [CLEAR_USER](state) {
    state.userInfo = null
    state.token = null
    localStorage.removeItem('token')
  }
}

const actions = {
  async login({ commit }, credentials) {
    try {
      const response = await api.login(credentials)
      commit(SET_TOKEN, response.token)
      commit(SET_USER, response.user)
      return response
    } catch (error) {
      throw error
    }
  },
  
  async logout({ commit }) {
    await api.logout()
    commit(CLEAR_USER)
  },
  
  async fetchUserInfo({ commit }) {
    const user = await api.getUserInfo()
    commit(SET_USER, user)
    return user
  }
}

const getters = {
  isLoggedIn: state => !!state.token,
  userName: state => state.userInfo?.name || '未登录',
  userAvatar: state => state.userInfo?.avatar || '/default-avatar.png'
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

购物车模块

javascript
// store/modules/cart.js
const state = () => ({
  items: [],
  checkoutStatus: null
})

const mutations = {
  pushProductToCart(state, { id, title, price }) {
    state.items.push({
      id,
      title,
      price,
      quantity: 1
    })
  },
  
  incrementItemQuantity(state, { id }) {
    const item = state.items.find(item => item.id === id)
    if (item) {
      item.quantity++
    }
  },
  
  setCartItems(state, items) {
    state.items = items
  },
  
  setCheckoutStatus(state, status) {
    state.checkoutStatus = status
  }
}

const actions = {
  addProductToCart({ state, commit }, product) {
    if (product.inventory > 0) {
      const cartItem = state.items.find(item => item.id === product.id)
      if (!cartItem) {
        commit('pushProductToCart', {
          id: product.id,
          title: product.title,
          price: product.price
        })
      } else {
        commit('incrementItemQuantity', cartItem)
      }
      commit('products/decrementProductInventory', { id: product.id }, { root: true })
    }
  },
  
  async checkout({ commit, state }) {
    const savedCartItems = [...state.items]
    commit('setCheckoutStatus', null)
    commit('setCartItems', [])
    
    try {
      await shop.buyProducts(savedCartItems)
      commit('setCheckoutStatus', 'successful')
    } catch (error) {
      commit('setCheckoutStatus', 'failed')
      commit('setCartItems', savedCartItems)
    }
  }
}

const getters = {
  cartProducts: state => state.items,
  
  cartTotalPrice: (state, getters) => {
    return getters.cartProducts.reduce((total, item) => {
      return total + item.price * item.quantity
    }, 0)
  },
  
  cartItemCount: state => state.items.reduce((count, item) => count + item.quantity, 0)
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

最佳实践

1. 始终使用命名空间

javascript
// ✅ 推荐
export default {
  namespaced: true,
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

// ❌ 不推荐 - 可能导致命名冲突
export default {
  state: { ... },
  mutations: { ... }
}

2. 使用模块工厂函数创建可复用模块

javascript
// 创建可复用的模块
function createListModule(options) {
  return {
    namespaced: true,
    state: () => ({
      items: [],
      loading: false,
      ...options.state
    }),
    mutations: {
      setItems(state, items) {
        state.items = items
      }
    },
    actions: {
      async fetchItems({ commit }) {
        const items = await options.fetchItems()
        commit('setItems', items)
      }
    }
  }
}

3. 合理组织模块结构

code
store/
├── index.js              # 入口文件
├── mutation-types.js     # Mutation 类型常量
├── modules/
│   ├── user.js          # 用户模块
│   ├── cart.js          # 购物车模块
│   ├── products.js      # 商品模块
│   └── index.js         # 模块导出
└── plugins/
    └── persist.js       # 持久化插件

4. 模块间通信

javascript
// 通过 rootState 和 rootGetters 访问其他模块
actions: {
  async checkout({ state, commit, rootState, rootGetters }) {
    const user = rootState.user.userInfo
    const isLoggedIn = rootGetters['user/isLoggedIn']
    
    if (!isLoggedIn) {
      throw new Error('请先登录')
    }
    
    // ...
  }
}

5. 动态模块的清理

javascript
// 组件销毁时清理动态模块
export default {
  created() {
    this.$store.registerModule('tempModule', tempModule)
  },
  
  beforeDestroy() {
    this.$store.unregisterModule('tempModule')
  }
}