模块化
当应用变得复杂时,使用模块化可以将 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>命名空间对比
| 访问方式 | 无命名空间 | 有命名空间 |
|---|---|---|
| State | state.user.userInfo | state.user.userInfo |
| Getter | getters.userName | getters['user/userName'] |
| Mutation | commit('setUserInfo') | commit('user/setUserInfo') |
| Action | dispatch('fetchUser') | dispatch('user/fetchUser') |
Module 命名空间实现原理
Vuex 通过 ModuleCollection 和 installModule 处理命名空间:
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')
}
}