九、将 Vuex 用于状态管理

在本章中,我们将使用Vuex来研究状态管理模式。Vuex可能不是每个创建的应用都需要,但非常重要的是,您必须了解什么是适合使用它的,以及如何实现它。

在本章结束时,您将完成以下工作:

  • 了解什么是Vuex以及为什么要使用它
  • 创建了您的第一个 Vuex 商店
  • 调查行动、突变、获取者和模块
  • 使用 Vue 开发工具在突变发生时逐步完成Vuex突变

什么是 Vuex?

状态管理是现代 web 应用的重要组成部分,随着应用的增长,管理这种状态是每个项目都面临的问题。Vuex希望通过强制实施集中存储来帮助我们实现更好的状态管理,这实际上是我们应用中的一个真实来源。它遵循类似于 Flux 和 Redux 的设计原则,还与官方 Vue 开发工具集成,以获得出色的开发体验。

到目前为止,我已经谈到了状态管理状态,但您可能仍然不清楚这对您的应用意味着什么。让我们更深入地定义这些术语。

状态管理模式(SMP)

我们可以将状态定义为组件或应用中变量/对象的当前值。如果我们将函数看作简单的INPUT -> OUTPUT机器,那么存储在这些函数之外的值就构成了应用的当前状态。

注意我是如何区分组件级别应用级别状态的。组件级状态可以定义为仅限于一个组件的状态(即组件中的数据函数)。应用级状态类似,但通常跨多个组件或服务使用。

随着应用的不断增长,跨多个组件传递状态变得更加困难。我们在本书前面看到,我们可以使用事件总线(即全局 Vue 实例)来传递数据,虽然这样做有效,但最好将状态定义为单一集中存储的一部分。这使我们能够更轻松地对应用中的数据进行推理,因为我们可以开始定义总是生成新版本状态的操作突变,并且管理状态变得更加系统化。

事件总线是一种依靠单一视图实例进行状态管理的简单方法,在小型 Vuex 项目中可能会有所帮助,但在大多数情况下,应使用 Vuex。随着我们的应用变得越来越大,使用 Vuex 清楚地定义我们的操作和预期的副作用可以让我们更好地管理和扩展项目。

下面的屏幕截图(中可以看到这一切是如何结合在一起的一个很好的例子 https://vuex.vuejs.org/en/intro.html

Vuex state flow

让我们将此示例分解为一个逐步的过程:

  1. 初始状态在 Vue 组件内部呈现。
  2. Vue 组件发送一个动作以从后端 API获取一些数据。
  3. 然后触发由突变处理的提交事件。此突变返回包含后端 API数据的新版本状态。
  4. 然后可以在 VueDevtools中看到该过程,您可以在应用中发生的前一状态的不同版本之间进行“时间旅行”。
  5. 新的状态随后在Vue 组件内部呈现。

因此,Vuex 应用的主要组件是 store,它是所有组件的唯一真实来源。存储可以读取,但不能直接更改;它必须具有突变功能才能进行任何改变。虽然这种模式一开始看起来很奇怪,但如果您以前从未使用过状态容器,这种设计允许我们以一致的方式向应用添加新功能。

由于 Vuex 本机设计用于 Vue,因此存储在默认情况下是被动的。这意味着,商店内发生的任何变化都可以实时查看,而无需任何黑客攻击。

关于国家的思考

作为一个思想练习,让我们从定义应用的目标以及任何状态、操作和潜在的变化开始。您还不必将以下代码添加到应用中,因此请继续阅读,我们将在最后将所有代码合并在一起。

让我们首先将状态视为键/值对的集合:

const state = {
 count: 0 // number
}

对于计数器应用,我们只需要当前计数的一个状态元素。这可能会有一个默认值0,类型为 number。由于这可能是我们的应用中唯一的状态,因此您可以将此状态视为应用级别。

接下来,让我们考虑一下用户可能希望使用计数器应用的任何操作类型。

然后可以将这三种操作类型分派到存储区,因此我们可以执行以下突变,每次都返回新版本的状态:

  • 增量:当前计数加一(0->1)
  • 减量:从当前计数中删除一个(1->0)
  • 复位:将当前计数设置回零(n->0)

我们可以想象,此时,我们的用户界面将更新为正确的绑定版本。让我们实现这一点并使之成为现实。

使用 Vuex

现在,我们已经详细了解了由Vuex驱动的应用的组成部分,让我们制作一个游乐场项目来利用这些功能!

在终端中运行以下命令:

# Create a new Vue project
$ vue init webpack-simple vuex-counter

# Navigate to directory
$ cd vuex-counter

# Install dependencies
$ npm install

# Install Vuex
$ npm install vuex

# Run application
$ npm run dev

创建新店

让我们首先在src/store中创建一个名为index.js的文件。这是我们将用来创建新商店并汇集各种组件的文件。

我们可以先导入VueVuex并告诉 Vue 我们想使用Vuex插件:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

然后,我们可以导出一个新的Vuex.Store,其中包含一个包含所有应用状态的状态对象。我们正在导出它,以便在必要时可以在其他组件中导入状态:

export default new Vuex.Store({
  state: {
    count: 0,
  },
});

定义动作类型

然后,我们可以在src/store中创建一个名为mutation-types.js的文件,其中包含用户在我们的应用中可能执行的各种操作:

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';

虽然我们不必像这样明确地定义我们的操作,但在可能的情况下使用常量是个好主意。这使我们能够更好地利用工具和绒线技术,并使我们能够一目了然地推断整个应用中的操作。

行动

我们可以使用这些动作类型提交一个新动作,然后由我们的突变处理。在src/store内创建一个名为actions.js的文件:

import * as types from './mutation-types';

export default {
  [types.INCREMENT]({ commit }) {
    commit(types.INCREMENT);
  },
  [types.DECREMENT]({ commit }) {
    commit(types.DECREMENT);
  },
  [types.RESET]({ commit }) {
    commit(types.RESET);
  },
};

在每个方法中,我们都会对返回的store对象进行解构,使其只接受commit函数。如果我们不这样做,我们必须像这样调用commit函数:

export default {
 [types.INCREMENT](store) {
  store.commit(types.INCREMENT);
 }
}

如果我们重温我们的状态图,我们可以看到在提交一个动作之后,该动作被 mutator 拾取。

突变

突变是唯一可以改变存储状态的方法;如前所述,这是通过提交/分派操作完成的。让我们在src/store中创建一个名为mutations.js的新文件,并添加以下内容:

import * as types from './mutation-types';

export default {
  [types.INCREMENT](state) {
    state.count++;
  },
  [types.DECREMENT](state) {
    state.count--;
  },
  [types.RESET](state) {
    state.count = 0;
  },
};

您将再次注意到,我们使用动作类型来定义方法名称;这可以通过 ES2015+的新功能命名计算属性名称实现。现在,无论何时提交/调度一个操作,mutator 都知道如何处理该操作并返回一个新状态。

吸气剂

我们现在可以提交操作,并让这些操作返回状态的新版本。下一步是创建 getter,这样我们就可以在应用中返回状态的切片部分。让我们在src/store中创建一个名为getters.js的新文件,并添加以下内容:

export default {
  count(state) {
    return state.count;
  },
};

我们有一个很小的例子,对这个属性使用 getter 并不是完全必要的,但是当我们扩展应用时,我们需要使用 getter 来过滤状态。将这些视为状态中值的计算属性,因此,如果我们希望为视图层返回此属性的修改版本,我们可以如下所示:

export default {
  count(state) {
    return state.count > 3 ? 'Above three!' : state.count;
  },
};

组合元素

为了将这一切结合起来,我们必须重新访问我们的store/index.js文件,并添加相应的stateactionsgettersmutations

import Vue from 'vue';
import Vuex from 'vuex';

import actions from './actions';
import getters from './getters';
import mutations from './mutations';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0,
  },
  actions,
  getters,
  mutations,
});

在我们的App.vue中,我们可以创建一个template,该template将为我们提供当前计数以及一些按钮到incrementdecrementreset状态:

<template>
  <div>
    <h1>{{count}}</h1>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">R</button>
  </div>
</template>

每当用户单击按钮时,将从以下方法之一中发送操作:

import * as types from './store/mutation-types';

export default {
  methods: {
    increment() {
      this.$store.dispatch(types.INCREMENT);
    },
    decrement() {
      this.$store.dispatch(types.DECREMENT);
    },
    reset() {
      this.$store.dispatch(types.RESET);
    },
  },
}

我们再次使用常量来获得更好的开发体验。接下来,为了利用前面创建的 getter,让我们定义一个computed属性:

export default {
  // Omitted
  computed: {
    count() {
      return this.$store.getters.count;
    },
  },
}

然后,我们有一个显示当前计数的应用,可以递增、递减或重置:

有效载荷

如果我们想让用户决定他们想增加多少计数呢?比如说,我们有一个文本框,我们可以添加一个数字,然后将计数增加那么多。如果文本框设置为0或为空,我们会将计数增加1

因此,我们的模板如下所示:

<template>
  <div>
    <h1>{{count}}</h1>

    <input type="text" v-model="amount">

    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">R</button>
  </div>
</template>

我们将 amount 值放在本地组件状态上,因为这不一定是主 Vuex 存储的一部分。这是一个重要的实现,因为这意味着我们仍然可以在必要时使用本地数据/计算值。我们还可以更新我们的方法,将数量传递给我们的行动/突变:

export default {
  data() {
    return {
      amount: 0,
    };
  },
  methods: {
    increment() {
      this.$store.dispatch(types.INCREMENT, this.getAmount);
    },
    decrement() {
      this.$store.dispatch(types.DECREMENT, this.getAmount);
    },
    reset() {
      this.$store.dispatch(types.RESET);
    },
  },
  computed: {
    count() {
      return this.$store.getters.count;
    },
    getAmount() {
      return Number(this.amount) || 1;
    },
  },
};

然后我们必须更新actions.js,因为它现在接收state对象和amount作为参数。当我们使用commit时,让我们也将amount传递给突变:

import * as types from './mutation-types';

export default {
  [types.INCREMENT]({ commit }, amount) {
    commit(types.INCREMENT, amount);
  },
  [types.DECREMENT]({ commit }, amount) {
    commit(types.DECREMENT, amount);
  },
  [types.RESET]({ commit }) {
    commit(types.RESET);
  },
};

因此,我们的突变看起来与以前相似,但这次我们根据数量增加/减少:

export default {
  [types.INCREMENT](state, amount) {
    state.count += amount;
  },
  [types.DECREMENT](state, amount) {
    state.count -= amount;
  },
  [types.RESET](state) {
    state.count = 0;
  },
};

塔达!现在,我们可以根据文本值增加计数:

Vuex 和 Vue 开发工具

现在,我们已经有了通过操作与商店交互的一致方式,我们可以利用 Vue devtools 查看我们的状态。如果您尚未安装 Vue 开发工具,请访问第 2 章正确创建 Vue 项目,了解更多相关信息。

我们将以计数器应用为例,以确保该项目正在运行,然后右键单击 Chrome(或您的浏览器的等效程序)中的 Inspect 元素。如果我们转到 Vue 选项卡并选择 Vuex,我们可以看到计数器已加载初始应用状态:

从前面的屏幕截图中,您可以看到 count state 成员以及任何 getter 的值。让我们单击“增量”按钮几次,看看会发生什么:

令人惊叹的我们可以看到增量操作以及对状态和 getter 的后续更改,以及关于突变本身的更多信息。让我们看看我们如何在全州进行时间旅行:

在前面的屏幕截图中,我选择了第一个动作的时间旅行按钮。然后您可以看到我们的状态被恢复为 count:1,这反映在元数据的其余部分中。然后更新应用以反映状态的变化,这样我们就可以逐级执行每个操作并在屏幕上看到结果。这不仅有助于调试,而且我们添加到应用中的任何新状态都将遵循相同的过程,并以这种方式可见。

让我们点击一个操作的提交按钮:

如您所见,这合并了我们的所有操作,直到我们点击 commit,然后成为基本状态的一部分。因此,count 属性等于您提交到基态的操作。

模块和可扩展性

目前,我们的一切都处于根状态。随着我们的应用越来越大,最好利用模块,这样我们就可以将容器适当地划分为不同的块。让我们通过在名为modules/countstore中创建一个新文件夹,将计数器状态转换为它自己的模块。

然后我们可以将actions.jsgetters.jsmutations.jsmutation-types.js文件移动到计数模块文件夹中。完成此操作后,我们可以在文件夹内创建一个index.js文件,该文件夹仅导出此模块的stateactionsgettersmutations

import actions from './actions';
import getters from './getters';
import mutations from './mutations';

export const countStore = {
  state: {
    count: 0,
  },
  actions,
  getters,
  mutations,
};

export * from './mutation-types';

我还选择从index.js文件中导出突变类型,因此我们可以通过仅从store/modules/count导入,在每个模块的基础上在组件中使用这些突变类型。因为我们在这个文件中导入了不止一个东西,所以我给了这个商店的名称countStore。让我们在store/index.js中定义新模块:

import Vue from 'vue';
import Vuex from 'vuex';
import { countStore } from './modules/count';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    countStore,
  },
});

我们的App.vue则略有变化;我们不引用 types 对象,而是专门引用此模块中的类型:

import * as fromCount from './store/modules/count';

export default {
  data() {
    return {
      amount: 0,
    };
  },
  methods: {
    increment() {
      this.$store.dispatch(fromCount.INCREMENT, this.getAmount);
    },
    decrement() {
      this.$store.dispatch(fromCount.DECREMENT, this.getAmount);
    },
    reset() {
      this.$store.dispatch(fromCount.RESET);
    },
  },
  computed: {
    count() {
      return this.$store.getters.count;
    },
    getAmount() {
      return Number(this.amount) || 1;
    },
  },
};

然后,通过使用与计数示例相同的文件/结构,我们可以向应用添加更多模块。这使我们能够随着应用的不断增长进行扩展。

总结

在本章中,我们利用Vuex库在 Vue 中进行一致的状态管理。我们定义了什么状态以及组件状态和应用级状态。我们学习了如何在不同的文件之间适当地分割我们的操作、getter、mutation 和 store,以实现可伸缩性,以及如何在组件中调用这些项。

我们还研究了在应用中使用带有Vuex的 Vue devtools 逐步完成突变。这使我们能够更好地调试/解释开发应用时所做的决策。

在下一章中,我们将介绍如何测试 Vue 应用,以及如何让测试驱动组件设计。