由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
const moduleA = {
state: { count: 0 },
mutations: {
increment(state) {
state.count++
}
},
getters: {
doubleCount(state) {
return state.count * 2
}
}
}
const moduleB = {
state: { todos: [] },
mutations: {
addTodo(state, text) {
state.todos.push({ text, done: false })
}
},
actions: {
addTodo({ commit }, text) {
commit('addTodo', text)
}
}
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
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,
state: { ... },
getters: {
isAdmin() { ... }
},
actions: {
login() { ... }
},
mutations: {
login() { ... }
},
modules: {
myPage: {
namespaced: true,
state: { ... },
getters: {
profile() { ... }
}
},
posts: {
namespaced: true,
state: { ... },
getters: {
popular() { ... }
}
}
}
}
}
})
启用命名空间后的访问方式:
// getter
this.$store.getters['account/isAdmin']
// action
this.$store.dispatch('account/login')
// mutation
this.$store.commit('account/login')
如果你希望使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。
modules: {
foo: {
namespaced: true,
getters: {
someGetter(state, getters, rootState, rootGetters) {
getters.someOtherGetter
getters['some/nested/module/someOtherGetter']
rootGetters.someOtherGetter
rootGetters['some/nested/module/someOtherGetter']
}
},
actions: {
someAction({ dispatch, commit, getters, rootGetters }) {
getters.someGetter
getters['some/nested/module/someGetter']
rootGetters.someGetter
rootGetters['some/nested/module/someGetter']
dispatch('someOtherAction')
dispatch('someOtherAction', null, { root: true })
commit('someMutation')
commit('someMutation', null, { root: true })
}
}
}
}
若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。
{
actions: {
someOtherAction({ dispatch }) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler(namespacedContext, payload) {
}
}
}
}
}
}
当使用 mapState, mapGetters, mapActions 和 mapMutations 这些辅助函数来绑定带命名空间的模块时,写起来可能比较繁琐:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo',
'some/nested/module/bar'
])
}
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo',
'bar'
])
}
可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions([
'foo',
'bar'
])
}
}
在 store 创建之后,你可以使用 store.registerModule 方法注册模块:
store.registerModule('myModule', {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
})
store.registerModule(['nested', 'myModule'], {
// ...
})
之后就可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态。
模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如 vuex-router-sync 插件就是通过动态注册模块将 Vue Router 和 Vuex 结合在一起。
你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。
在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState 选项将其归档:store.registerModule('auth', module, { preserveState: true })。
有时我们可能需要创建一个模块的多个实例:
如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被污染。
实际上这和 Vue 组件内的 data 是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态:
const MyReusableModule = {
state() {
return {
foo: 'bar'
}
},
// mutation, action 和 getter 等等...
}
const userModule = {
namespaced: true,
state() {
return {
user: null,
token: null,
isLoggedIn: false
}
},
getters: {
userName: state => state.user?.name || '游客',
userAvatar: state => state.user?.avatar || '/default-avatar.png',
isAdmin: state => state.user?.role === 'admin'
},
mutations: {
SET_USER(state, user) {
state.user = user
state.isLoggedIn = !!user
},
SET_TOKEN(state, token) {
state.token = token
},
CLEAR_USER(state) {
state.user = null
state.token = null
state.isLoggedIn = false
}
},
actions: {
async login({ commit }, credentials) {
const response = await api.login(credentials)
commit('SET_USER', response.user)
commit('SET_TOKEN', response.token)
return response
},
async logout({ commit }) {
await api.logout()
commit('CLEAR_USER')
}
}
}
export default userModule
const cartModule = {
namespaced: true,
state() {
return {
items: [],
checkoutStatus: null
}
},
getters: {
cartTotal: state => {
return state.items.reduce((total, item) => {
return total + item.price * item.quantity
}, 0)
},
cartItemCount: state => {
return state.items.reduce((count, item) => count + item.quantity, 0)
}
},
mutations: {
PUSH_PRODUCT_TO_CART(state, { product }) {
state.items.push({
id: product.id,
title: product.title,
price: product.price,
quantity: 1
})
},
INCREMENT_ITEM_QUANTITY(state, { id }) {
const cartItem = state.items.find(item => item.id === id)
if (cartItem) {
cartItem.quantity++
}
},
SET_CART_ITEMS(state, items) {
state.items = items
},
SET_CHECKOUT_STATUS(state, status) {
state.checkoutStatus = status
}
},
actions: {
addProductToCart({ state, commit }, product) {
if (product.inventory > 0) {
const cartItem = state.items.find(item => item.id === product.id)
if (!cartItem) {
commit('PUSH_PRODUCT_TO_CART', { product })
} else {
commit('INCREMENT_ITEM_QUANTITY', cartItem)
}
}
},
async checkout({ commit, state }) {
commit('SET_CHECKOUT_STATUS', null)
try {
await api.buyProducts(state.items)
commit('SET_CART_ITEMS', [])
commit('SET_CHECKOUT_STATUS', 'successful')
} catch (error) {
commit('SET_CHECKOUT_STATUS', 'failed')
}
}
}
}
export default cartModule
const productsModule = {
namespaced: true,
state() {
return {
all: [],
loading: false,
error: null
}
},
getters: {
availableProducts: state => {
return state.all.filter(product => product.inventory > 0)
},
getProductById: state => id => {
return state.all.find(product => product.id === id)
}
},
mutations: {
SET_PRODUCTS(state, products) {
state.all = products
},
SET_LOADING(state, loading) {
state.loading = loading
},
SET_ERROR(state, error) {
state.error = error
},
DECREMENT_PRODUCT_INVENTORY(state, { id }) {
const product = state.all.find(product => product.id === id)
if (product) {
product.inventory--
}
}
},
actions: {
async getAllProducts({ commit }) {
commit('SET_LOADING', true)
try {
const products = await api.getProducts()
commit('SET_PRODUCTS', products)
} catch (error) {
commit('SET_ERROR', error.message)
} finally {
commit('SET_LOADING', false)
}
}
}
}
export default productsModule
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)
export default new Vuex.Store({
modules: {
user,
cart,
products
},
strict: process.env.NODE_ENV !== 'production'
})
推荐的项目结构:
store/
├── index.js # 组装模块并导出 store
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
├── getters.js # 根级别的 getter
└── modules/
├── user.js # 用户模块
├── cart.js # 购物车模块
├── products.js # 产品模块
└── orders.js # 订单模块
每个模块应该只负责一个业务领域:
// 用户相关状态
const userModule = { ... }
// 产品相关状态
const productsModule = { ... }
// 购物车相关状态
const cartModule = { ... }
始终启用命名空间以避免命名冲突:
const myModule = {
namespaced: true,
// ...
}
通过根状态或 action 进行模块间通信:
const moduleA = {
actions: {
async fetchData({ commit, rootState }) {
const userId = rootState.user.id
const data = await api.fetchData(userId)
commit('SET_DATA', data)
}
}
}
使用常量定义模块名称和类型:
export const MODULE_NAMES = {
USER: 'user',
CART: 'cart',
PRODUCTS: 'products'
}
export const USER_TYPES = {
SET_USER: 'SET_USER',
SET_TOKEN: 'SET_TOKEN',
CLEAR_USER: 'CLEAR_USER'
}