Vue 2 中 Store (Vuex) 从入门到精通
一、Vuex 核心概念与工作流
Vuex 是 Vue 2 官方推荐的状态管理模式,采用集中式存储管理应用的所有组件状态。我们可以把它理解为一个"全局数据仓库",配合严格的规则保证状态以可预测的方式发生变化。
核心工作流程
dispatch commit 修改 渲染 派生 计算属性 组件 Actions Mutations State Getters
核心概念解析
| 模块 | 类型 | 描述 | 是否同步 | 是否可修改 state |
|---|---|---|---|---|
| State | 数据源 | 存储应用的状态数据 | - | ❌ |
| Getter | 计算属性 | 对 state 进行派生计算 | ✅ | ❌ |
| Mutation | 同步方法 | 唯一能修改 state 的方式 | ✅ | ✅(唯一方式) |
| Action | 异步方法 | 提交 mutation,处理异步操作 | ❌(内部可异步) | ❌(通过提交 mutation) |
| Module | 分割模块 | 将 store 拆分为多个模块 | - | ✅ |
二、Vuex 基础使用
1. 安装与配置
bash
# Vue 2 必须安装 Vuex 3.x 版本
npm install vuex@3 --save
# 或
yarn add vuex@3
2. 创建 Store
在 src/store/index.js 中创建 store 实例:
javascript
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// 状态数据
state: {
count: 0,
user: null
},
// 同步修改状态
mutations: {
INCREMENT(state) {
state.count++
},
SET_USER(state, user) {
state.user = user
}
},
// 异步操作
actions: {
login({ commit }, credentials) {
return api.login(credentials).then(user => {
commit('SET_USER', user)
})
}
},
// 计算属性
getters: {
doubleCount: state => state.count * 2,
isLoggedIn: state => !!state.user
},
// 模块化(可选)
modules: {
// cart: cartModule
}
})
3. 注入 Vue 实例
在 main.js 中挂载 store:
javascript
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
store, // 注入所有组件
render: h => h(App)
}).$mount('#app')
4. 在组件中使用
访问状态 (State)
vue
<template>
<div>
<p>Count: {{ $store.state.count }}</p>
<p>User: {{ $store.state.user?.name }}</p>
</div>
</template>
使用 Getter
vue
<template>
<p>Double Count: {{ $store.getters.doubleCount }}</p>
<p>Logged In: {{ $store.getters.isLoggedIn ? 'Yes' : 'No' }}</p>
</template>
提交 Mutation (同步)
vue
<script>
export default {
methods: {
increment() {
this.$store.commit('INCREMENT')
},
setUser(user) {
this.$store.commit('SET_USER', user)
}
}
}
</script>
分发 Action (异步)
vue
<script>
export default {
methods: {
handleLogin(credentials) {
this.$store.dispatch('login', credentials)
.then(() => {
// 登录成功后的操作
})
}
}
}
</script>
三、进阶使用:辅助函数与模块化
1. 辅助函数简化代码
Vuex 提供了 mapState、mapGetters、mapMutations 和 mapActions 辅助函数,简化组件中对 Vuex 的使用。
vue
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
// 映射 state
...mapState(['count', 'user']),
// 映射 getters
...mapGetters(['doubleCount', 'isLoggedIn'])
},
methods: {
// 映射 mutations
...mapMutations(['INCREMENT', 'SET_USER']),
// 映射 actions
...mapActions(['login']),
// 使用映射的方法
handleIncrement() {
this.INCREMENT()
},
handleLoginSubmit(credentials) {
this.login(credentials)
}
}
}
</script>
2. 模块化管理
随着应用规模增长,单一 store 会变得臃肿,模块化是必然选择。
创建模块
javascript
// store/modules/user.js
export default {
namespaced: true, // 启用命名空间,避免模块间命名冲突
state: () => ({
profile: null,
permissions: []
}),
mutations: {
SET_PROFILE(state, profile) {
state.profile = profile
},
SET_PERMISSIONS(state, permissions) {
state.permissions = permissions
}
},
actions: {
async fetchProfile({ commit }) {
const profile = await api.getUserProfile()
commit('SET_PROFILE', profile)
}
},
getters: {
hasPermission: (state) => (permission) => {
return state.permissions.includes(permission)
}
}
}
注册模块
javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
cart
}
})
使用带命名空间的模块
vue
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
computed: {
...mapGetters('user', ['hasPermission'])
},
methods: {
...mapActions('user', ['fetchProfile']),
checkPermission() {
return this.hasPermission('edit-content')
},
loadUserProfile() {
this.fetchProfile()
}
},
created() {
this.loadUserProfile()
}
}
</script>
四、最佳实践与高级技巧
1. 使用常量命名 Mutations/Actions
javascript
// store/mutation-types.js
export const SET_USER = 'SET_USER'
export const INCREMENT = 'INCREMENT'
// 在 mutations 中使用
import * as types from './mutation-types'
mutations: {
[types.SET_USER](state, user) {
state.user = user
},
[types.INCREMENT](state) {
state.count++
}
}
2. 状态持久化
Vuex 状态在页面刷新后会丢失,可通过 vuex-persistedstate 插件解决:
bash
npm install vuex-persistedstate --save
javascript
// store/index.js
import createPersistedState from 'vuex-persistedstate'
export default new Vuex.Store({
// ...其他配置
plugins: [
createPersistedState({
// 只持久化特定模块
paths: ['user', 'cart'],
// 使用 sessionStorage 而不是 localStorage
storage: window.sessionStorage
})
]
})
3. 严格模式
在开发环境启用严格模式,防止直接修改 state:
javascript
export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
// ...其他配置
})
4. 调试工具
Vue DevTools 提供了强大的 Vuex 调试功能,包括:
- 查看状态树
- 追踪状态变化历史
- 时间旅行调试(回溯到之前的状态)
确保在开发环境启用:
javascript
// main.js
Vue.config.devtools = true;
五、常见问题与解决方案
1. 状态更新但视图未更新
原因:Vue 无法检测对象属性的直接添加/删除或数组索引的直接修改。
解决方案:
javascript
// 正确修改对象
this.$store.commit('UPDATE_USER', { ...state.user, name: 'New Name' })
// 正确修改数组
mutations: {
ADD_ITEM(state, item) {
state.items.push(item) // 使用数组方法
// 或
Vue.set(state.items, index, newValue)
}
}
2. 模块化后辅助函数使用问题
解决方案:使用带命名空间的辅助函数或 createNamespacedHelpers
javascript
import { createNamespacedHelpers } from 'vuex'
const { mapActions, mapGetters } = createNamespacedHelpers('user')
export default {
computed: {
...mapGetters(['hasPermission'])
},
methods: {
...mapActions(['fetchProfile'])
}
}
3. 跨模块通信
解决方案:使用 root 参数访问根模块
javascript
// 在模块的 action 中访问根模块
actions: {
async someAction({ commit, dispatch, rootState }) {
// 访问根模块状态
console.log(rootState.appVersion)
// 调用根模块 action
await dispatch('fetchGlobalData', null, { root: true })
// 提交根模块 mutation
commit('SET_GLOBAL_STATE', data, { root: true })
}
}
六、Vuex 与 Vue Router 结合使用
在路由守卫中使用 Vuex 管理用户认证状态:
javascript
// router/index.js
import VueRouter from 'vue-router'
import store from '@/store'
const router = new VueRouter({
// 路由配置
})
router.beforeEach((to, from, next) => {
// 检查路由是否需要认证
if (to.matched.some(record => record.meta.requireAuth)) {
// 从 Vuex 获取登录状态
if (!store.getters.isLoggedIn) {
// 未登录,重定向到登录页
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
// 已登录,检查权限
if (to.meta.permission && !store.getters['user/hasPermission'](to.meta.permission)) {
next({ path: '/403' })
} else {
next()
}
}
} else {
next()
}
})
export default router