前言
在应用程序中,我们通常采用 action
=> state
=> view
的模式,来更新数据和视图。由于“单向数据流”的简洁性,使得开发者能够轻松追踪到各个数据的流向和状态变化。然而,在一些复杂的应用程序中,比如具有多个视图和嵌套组件的应用场景,这种模式的简洁性很容易被破坏。这时,我们需要一个全局状态管理模式来管理应用程序中的状态,以保证状态的一致性和可追踪性,同时降低组件间的耦合度。
以下是单向数据流图:

(图片来源于 vuex 官网)
Vuex 是一个专为 Vue 复杂应用程序开发的状态管理工具。它采用集中式存储,来管理应用的所有组件的状态,并以相应的规则保证状态以一种可追踪的方式管理。当多个组件需要共享相同的状态时,使用 Vuex 可以让这些组件共享同一个状态树,避免了传递状态数据的复杂性,从而使得状态管理更加简单和可维护。同时,Vuex 也适用于 Taro 跨端项目,可以帮助开发者更好地管理跨端项目中的状态,提高开发效率和代码可维护性。
下图是 Vuex 的实现原理图:

(图片来源于 vuex 官网)
快速开始
基于 Taro v3.5.6, Vue2, vuex v3.0.0。
安装
首先请安装 vuex
的 3.x 版本:
1 2 3
| $ yarn add vuex@^3
$ npm install vuex@^3
|
使用
在项目中新建 src/store/index.js
文件用来配置 store
,根据需求设置 state
、mutations
、actions
、getters
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
import Vue from "vue"; import Vuex from "vuex";
Vue.use(Vuex);
const state = { count: 0, };
const mutations = { addCount(state) { state.count++; }, };
const actions = { addCount(context) { context.commit("addCount"); }, };
const getters = { getCount(state) { return state.count; }, };
export default new Vuex.Store({ state, mutations, actions, getters, });
|
为了在组件中可以通过 this.$store
全局获取到 store 中的内容,我们需要把 store 注入到 Vue 实例中:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
import Vue from "vue"; import store from "./store";
const App = { store, render(h) { return h("block", this.$slots.default); }, };
export default App;
|
接下来,即可以在 Vue 组件中使用 store 了,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <view class="index"> <button @tap="handleTap">数字增加</button> <view>{{ getCount }}</view> </view> </template>
<script> import { mapGetters, mapActions } from "vuex"; // mapGetters, mapActions 是 vuex 提供的辅助函数 export default { computed: { ...mapGetters(["getCount"]), // 把 `this.getCount` 映射为 `this.$store.getters.getCount` }, methods: { ...mapActions(["addCount"]), // 将 `this.addCount()` 映射为 `this.$store.dispatch('addCount')` handleTap() { this.addCount(); // 触发 action }, }, }; </script>
|
数据模块化管理
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
从上文【快速开发】模块可知,所有应用程序的数据和状态都是统一维护在 src/store/index.js
文件中,所以当应用程序变得复杂时,将会变得难以维护。
为此,vuex
提供了一种通过命名空间来进行模块化的管理方案,被分割的每个模块都拥有自己的 state
、mutation
、action
、getter
、甚至是嵌套子模块。下面是具体的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
import moduleGlobal from "./global"
const moduleCount = { namespaced: true, state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } }
const store = new Vuex.Store({ modules: { global: moduleGlobal, count: moduleCount } })
|
在 Vue 组件中使用模块化 store ,需要添加模块的命名空间参数,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <view class="index"> <button @tap="handleTap">数字增加</button> <view>{{ getCount }}</view> </view> </template>
<script> import { mapGetters, mapActions } from "vuex"; // mapGetters, mapActions 是 vuex 提供的辅助函数 export default { computed: { ...mapGetters("count", ["getCount"]), // 第一个参数为命名空间,把 `this.getCount` 映射为 `this.$store.getters['count/getCount']` }, methods: { ...mapActions("count", ["addCount"]), // 第一个参数为命名空间,将 `this.addCount()` 映射为 `this.$store.dispatch('count/addCount')` handleTap() { this.addCount(); // 触发 action }, }, }; </script>
|
案例场景
在实际应用开发中,存在着许多复杂的应用场景需要使用 vuex 进行数据状态管理,如登录、表单校验、偏好设置、路由管理等常见场景。
下面将从实际场景出发,逐步介绍如何使用 vuex 进行数据状态管理。
1、用户登录信息全局共享和管理
应用程序需要处理用户登录和授权,涉及到用户的身份认证、用户信息和权限等相关信息的管理。在这种情况下,使用 Vuex 可以将这些信息存储在全局状态树中,并在多个组件之间共享这些信息。
在实际项目中,通常会对登录和退出登录模块进行统一的封装和处理,保持用户信息能全局共享。例如在登录页 pages/login/index
中,进行用户登录,并将用户信息更新到全局数据中。而在个人信息页 pages/user/index
中进行退出登录,并将用户信息从全局数据中清除。
下图展示了登录和退出登录的简易流程:

以下是具体实现代码:
1、采用 vuex 模块化,将用户信息数据在 store 中独立管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
const defaultUser = { name: "游客", age: "18", role: "visitor", isLogin: false, };
const state = { user: defaultUser, };
const mutations = { changeUser(state, payload) { state.user = payload; }, clearUser(state) { state.user = defaultUser; }, };
const actions = { changeUser(context, payload) { context.commit("changeUser", payload); }, clearUser(context) { context.commit("clearUser"); }, };
const getters = { getUser(state) { return state.user; }, };
export default { namespaced: true, state, mutations, actions, getters, };
|
导出完整的 store 模块。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
import Vue from "vue"; import Vuex from "vuex"; import moduleUser from "./modules/user"; Vue.use(Vuex);
export default new Vuex.Store({ modules: { user: moduleUser, }, });
|
2、登录页模块 pages/login/index
,负责调用登录接口,并更新用户信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <view class="index"> <view v-if="getUser.isLogin"> <view>姓名:{{ getUser.name }}</view> <view>年龄:{{ getUser.age }}</view> <view>角色:{{ getUser.role }}</view> </view> <view v-else> <view>未登录</view> </view> <button @tap="handleLogin">点击登录</button> </view> </template>
<script> import { mapGetters, mapActions } from "vuex"; // mapGetters, mapActions 是 vuex 提供的辅助函数
/** 模拟登录接口 */ function login() { return Promise.resolve({ name: "张三", age: 30, role: "dev", isLogin: true }); } export default { computed: { ...mapGetters("user", ["getUser"]), }, methods: { ...mapActions("user", ["changeUser"]), handleLogin() { // 登录接口 login().then((res) => { // 更新用户信息 const { name, age, role, isLogin } = res; this.changeUser({ name, age, role, isLogin }); // 触发 vuex 的 action }); }, }, }; </script>
|
3、用户信息页 pages/user/index
,负责调用退出登录接口,并清空当前用户信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <template> <view class="index"> <view v-if="getUser.isLogin"> <view>姓名:{{ getUser.name }}</view> <view>年龄:{{ getUser.age }}</view> <view>角色:{{ getUser.role }}</view> </view> <view v-else> <view>未登录</view> </view> <button @tap="handleLogout">退出登录</button> </view> </template>
<script> import { mapGetters, mapActions } from "vuex"; // mapGetters, mapActions 是 vuex 提供的辅助函数
/** 模拟退出登录接口 */ function logout() { return Promise.resolve(); } export default { computed: { ...mapGetters("user", ["getUser"]), }, methods: { ...mapActions("user", ["clearUser"]), handleLogout() { // 退出登录接口 logout().then((res) => { // 清空用户信息 this.clearUser(); // 触发 vuex 的 action }); }, }, }; </script>
|
2、表单数据多模块共享
应用程序需要处理复杂的表单数据和验证逻辑,例如表单的动态添加、删除、编辑等。在这种情况下,使用 Vuex 可以将表单数据存储在全局状态树中,并在多个组件之间共享表单数据和验证逻辑。
在移动端,通常会使用自定义的卡片样式来展示数据列表。当我们需要修改大量数据表单时,需要跳转到对应的表单详情页进行修改。修改完成后,我们会返回列表页,此时数据列表中对应的表单数据应该已经被更新。但是,在弱网情况下,接口数据返回较慢,导致回到列表页后数据变更缓慢或无法变更。
为了解决这种场景下的问题,我们可以使用 Vuex 实现数据的无感知更新,从而获得更流畅的用户体验。流程图如下:

下面是实现代码的参考:
1、采用 vuex 模块化,将列表信息数据在 store 中独立管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const state = { moduleA: { a: [1, 2, 3, 4], b: [11, 22, 33, 44], }, moduleB: {}, };
const mutations = { changeModuleA(state, payload) { state.moduleA[payload.name] = payload.value; }, };
const actions = { changeModuleA(context, payload) { context.commit("changeModuleA", payload); }, };
const getters = { getModuleA(state) { return state.moduleA; }, };
export default { namespaced: true, state, mutations, actions, getters, };
|
2、列表页 pages/list/index
,根据 store 中模块数据遍历展示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template> <view class="index"> <view v-for="(value, name) in getModuleA" :key="name"> <view>模块:{{ name }}</view> <view v-for="item in value" :key="item">{{ item }}</view> <button @tap="() => handleDetail(name)">编辑</button> </view> </view> </template>
<script> import { mapGetters } from "vuex"; import Taro from "@tarojs/taro"; export default { onShow() { // 这里可以通过接口获取数据并做异步更新 this.$forceUpdate(); // 因为 back 回退不会触发页面重新渲染,所以需要强制更新 vue 数据 }, computed: { ...mapGetters("list", ["getModuleA"]), }, methods: { handleDetail(moduleName) { // 路由跳转到编辑详情页 Taro.navigateTo({ url: `/pages/list-detail/index?moduleName=${moduleName}`, }); }, }, }; </script>
|
3、列表详情页 pages/list-detail/index
,详情页中进行表单数据修改,并在 store 中进行数据更新。更新成功后,Vuex 会响应式触发步骤 2 中的列表数据重新渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <template> <view class="index"> <view>模块:{{ moduleName }}</view> <input v-for="(item, index) in getModuleA[moduleName]" :key="item" @input="(e) => handleinput(e, index)" :value="item" placeholder="请输入" /> <button @tap="handleSave">保存</button> </view> </template>
<script> import { mapGetters, mapActions } from "vuex"; // mapGetters, mapActions 是 vuex 提供的辅助函数 import Taro from "@tarojs/taro"; export default { onShow() { // 获取路由参数 const params = Taro.getCurrentInstance().router.params; this.moduleName = params.moduleName; }, data() { return { moduleName: "", dataList: [] }; }, computed: { ...mapGetters("list", ["getModuleA"]), }, methods: { ...mapActions("list", ["changeModuleA"]), // 修改数据 handleinput(e, index) { const value = e.detail.value; this.dataList[index] = value; }, // 保存数据 handleSave() { const storeData = this.getModuleA[this.moduleName]; // store 中数据 const newData = Object.assign(storeData, this.dataList); // 合并后的新数据 this.changeModuleA({ name: this.moduleName, value: newData }); // 触发 vuex 的 action // 这里可以做接口数据的分布提交 // 保存后回到列表页 Taro.navigateBack(); }, }, }; </script>
|
3、偏好设置(语言、主题)全局切换
应用程序需要管理用户偏好和设置,例如语言、主题和字体大小等。在这种情况下,使用 Vuex 可以将用户偏好和设置状态存储在全局状态树中,并在多个组件之间共享和更新这些状态。
在应用程序中,用户的偏好和设置通常会影响整个应用程序的外观和行为,因此需要集中管理和共享这些状态。使用 Vuex 可以将用户偏好和设置状态存储在全局状态树中,并在多个组件之间共享和更新这些状态,从而使代码更加模块化和易于维护。
具体而言,可以在 Vuex 中创建一个单独的 store 对象来存储用户偏好和设置状态。在该对象中,可以定义一些 mutation 和 action 方法来更新和操作用户偏好和设置状态。例如,可以定义一个 SET_LANGUAGE
mutation 方法来更新应用程序的语言设置,或者定义一个 SET_THEME
mutation 方法来更新应用程序的主题设置。当用户更改偏好或设置时,可以触发一个 mutation 方法来更新状态,并将新的状态存储在全局状态树中。同时,可以在需要访问用户偏好或设置的组件中使用 Vuex 提供的 mapState 方法来获取状态,从而更新视图。
通过将用户偏好和设置状态存储在全局状态树中,并在多个组件之间共享和更新这些状态,可以使代码更加模块化和易于维护。这样,不仅可以避免在多个组件之间传递属性或事件的复杂性,还可以将代码更好地组织在一起,使其更容易理解和修改。
4、路由状态管理
应用程序需要处理多级路由和视图的导航和状态管理。在这种情况下,使用 Vuex 可以将当前的路由状态存储在全局状态树中,并在多个组件之间共享和更新这些状态。
在这种场景下,Vuex 可以将当前的路由状态存储在全局状态树中。通过这种方式,可以在应用程序中的任何组件中访问路由状态,无需通过属性或事件传递路由状态。这样可以避免组件之间的耦合度,使得代码更加清晰和易于维护。
具体而言,Vuex 中可以使用一个单独的 store 对象来存储路由状态。在该对象中,可以定义一些 mutation 和 action 方法来更新和操作路由状态。当路由发生变化时,可以触发一个 mutation 方法来更新路由状态,并将新的状态存储在全局状态树中。同时,可以在需要访问路由状态的组件中使用 Vuex 提供的 mapState 方法来获取路由状态,从而更新视图。
最后
虽然 vuex
可以帮助我们有效地管理共享状态并简化大量应用场景的实现,但对于简单的应用程序而言,使用vuex
反而会增加繁琐和冗余。因此,在考虑使用vuex
之前,我们应该权衡短期和长期效益。
参考资料