七、使用 Vuex

在本章中,我们将学习如何使用 Vuex 管理 Vue 中的复杂状态。Vuex 帮助处理 Vue 应用程序中管理状态和深度嵌套组件的问题。

在本章结束时,您将了解 Vuex 解决了哪些问题以及它是如何解决这些问题的,并且您应该了解所有运动部件的安装位置。您还将了解如何构建一个简单的 Vuex 应用程序,以及在考虑扩展它时要采取的方法。

具体来说,我们将讨论以下主题:

  • 理解状态
  • 状态管理、数据存储和单向数据流
  • 热装
  • 构建一个非常简单的 Vuex 应用程序
  • 如何从 Vue DevTools 的 Vuex 选项卡更新状态
  • 构建更复杂的 Vuex 应用程序

让我们从准确理解状态开始。

什么是国家?

应用程序的状态是其在某个时间点的所有数据。由于我们通常关心当前应用程序的状态,我们可以将其重新表述为:该状态是应用程序的数据,与现在一样,是由我们的应用程序采取的先前步骤产生的,并且基于应用程序内部响应与其交互的用户的功能

那么,在我们的应用程序中,是什么改变了它的状态呢?当然是功能。用户与我们的应用程序交互,触发函数将当前状态更改为其他状态。

然而,随着我们的应用程序的发展,组件嵌套在几个层次上的情况并不少见。如果我们说状态是我们的应用程序在任何给定时间在屏幕上的显示方式的真相来源,那么让这个真相来源尽可能容易推理和使用将对我们有利。

不幸的是,在复杂的应用程序中,这并不容易。我们应用程序的任何部分,我们应用程序中的任何组件都可能影响我们应用程序的任何其他部分。在我们的应用程序中管理状态变得有点像打鼹鼠游戏:在我们的应用程序的某个部分中的交互将导致在我们的应用程序的其他部分中出现其他东西

关于如何在前端应用程序中管理复杂状态的最佳实践的推理产生了诸如数据存储单向数据流等概念。

状态管理、数据存储和单向数据流

对于管理复杂状态的问题,一个常见的解决方案是一个存储的想法:一个单一的真相来源,保存我们应用程序状态的所有数据。一旦我们有了中心位置商店,我们就可以更容易地对状态进行推理,因为现在只需将状态数据发送到应用程序生命周期中任何时候都需要的组件。

为了简化状态更新,我们需要限制进行这些更新的方式。这就是单向数据流的用武之地。对于单向数据流,我们指定了关于数据在应用程序中如何流动的规则,这意味着现在只有那么多预期的方式可以让数据(状态)在应用程序中流动,这使得在需要时更容易对状态进行推理和调试状态。这种方法也节省了大量的时间,因为现在我们作为开发人员知道应该期待什么;也就是说,寻找我们知道状态是可变的点。

Vuex 状态管理模式

Vuex 是 Vue 的一个插件,由 Vue 的核心团队开发。安装非常简单。如果您需要快速原型,只需从 CodePen online editor 内的设置添加 Vuex 库即可,如第 1 章介绍 Vue中所述。

您还可以使用以下命令通过 npm 进行安装:

npm install --save vuex

当您试图了解 Vuex 的工作原理时,通常会在网上找到一些参考资料,这些参考资料将 Vuex 描述为一种受流量严重影响的状态管理模式。这在一定程度上是正确的,但有趣的是,Flux 本身就是受 Elm 建筑的启发。尽管如此,在 Vuex 中,数据流如下所示:

  • 查看组件动作
  • 对突变的作用
  • 突变到状态
  • 状态查看组件

数据总是以一种方式流动,在开始的地方结束,对组件进行更新,然后调度动作,然后提交突变,然后突变状态,然后呈现组件,循环重复。因此,从稍微不同的角度来看单向数据流,我们可以对其进行重新表述,重点放在描述存储中的数据发生了什么变化的动词上:

  • 动作被调度
  • 突变是承诺的
  • 状态为突变
  • 组件被呈现

再看一下单向数据流,我们现在可以使用以下名词来描述数据流:组件动作突变状态。使用动词来描述数据流,我们可以将此过程视为:调度提交突变呈现

在 Vuex 中查看数据流的这两种方式都是同一个硬币的两面,状态更新的周期相同,因此将这两个短列表提交到内存中不会有什么坏处,因为这将有助于加快对 Vuex 基本概念的理解。

为了直观地强化这些解释,官方 Vuex 文档中提供了此单向数据流的图表,网址为:https://vuex.vuejs.org/vuex.png

你可能会问,为什么要采用这种间接方法?为什么组件不能直接改变状态?这有两个主要原因:首先,由于异步代码在 JavaScript 世界中只是一个事实,因此选择在 Vuex 中分离异步和同步操作。因此,操作被设置为异步,因此它们可以,例如,从服务器获取一些数据,并且只有当此异步数据获取完成时,它们才能commit突变;因为突变是严格同步的,所以在对服务器的调用完成之前调用它们是没有意义的。其次,这种分离关注点的方法可以更容易地跟踪状态更改,甚至包括时间旅行调试:按时间顺序重新运行突变以跟踪状态更改并查找 bug。

In the Vuex state management pattern, components can never directly mutate global state. Mutations do that.

在下一节中,我们将研究每一个构建块。

商店

需要将存储添加到 Vue 实例根目录中,以便所有组件都可以共享此集中式全局状态。通常,我们将存储声明为const,然后在稍后的代码中,我们将其添加到作为参数传递给 Vue 构造函数的对象文本中,如下所示:

const store = new Vuex.Store({ 
  // store details go here
})
new Vue({
 el: '#app',
 store: store,
 // etc
})

接下来,我们将学习 getter。

Vuex 商店中的 Getters

我们的商店也可以有吸气剂。getter 允许我们从模板中的状态返回值。它们有点像计算值。它们是只读的,这意味着它们无法更改状态。他们的责任只是阅读并对其进行一些非破坏性的操作。然而,基础数据并没有发生变异。

因此,我们使用存储中的 getter 对全局状态执行一些非破坏性工作。那我们怎么处理他们呢?我们如何使用它们?我们在应用程序的另一端使用它们——在一个组件中使用computed并从应用商店返回 getter 的值。

Vuex 存储突变

突变,顾名思义,突变状态,是同步的。改变状态的函数接收参数:现有状态和有效负载。payload 参数是可选的。他们负责直接更新 Vuex 中的状态。您可以使用以下语法执行操作的变异:state.commit

Vuex 存储中的操作

操作通过调用我们在存储中定义的一个或多个突变,异步和间接地更新状态。因此,行动可以根据需要调用尽可能多的突变。另一方面,在组件内部,为了执行操作,我们使用存储的分派值,使用以下语法:store.dispatch

现在,让我们扩展样板代码,以包括刚才讨论的内容:

const store = new Vuex.Store({ 
  // store details go here; they usually have:
  state: {
    // state specified here
  },
  getters: {
    // getters are like computed values - they don't mutate state
 },
 mutations: {
   // they mutate the state and are synchronous, 
   // functions that mutate state can have arguments; these arguments are called 'payload'
 },
 actions: {
   // asynchronous functions that commit mutations
 }
})
new Vue({
 el: '#app',
 store,
 // etc
})

正如我们在 Vue 构造函数中看到的,使用 ES6 语法,可以简化构造函数的对象文字参数中的[T0]键值对,只需使用[T1]。

热装

Webpack 的兴起带来的另一个流行概念是热重新加载。当你的应用程序运行时,例如,在更新文件时,在你的一个组件网页中添加一些对范围样式的更改,将在不使用应用程序中的状态的情况下重新加载更新的文件。换句话说,它不会重新加载整个页面,而是只加载受更改影响的应用程序部分。这之所以有用,是因为在热模块更换时,状态将保持不变,而如果刷新页面,则不可能保持不变。这带来了更快的开发时间和浏览器中无缝更新体验的额外好处。

使用 Vuex 构建水果柜台应用程序

我们将构建的应用程序只是一个简单的水果计数器应用程序。我们的目标是帮助用户确保每天吃五片水果,我们将建立一个简单的应用程序,从五片水果开始,每次我们点击按钮,它会将数量减少1。这样,我们就可以跟踪我们的健康饮食目标。

我们将通过设置初始状态开始应用程序,其中只有一个键值对:

const store = new Vuex.Store({
  state: {
    count: 5
  },

接下来,我们将设置getters。我们已经了解到,getters仅返回状态:

  getters: {
    counter(state) {
      return state.count;
    }
  },

接下来,我们将添加两个突变:第一个突变,decrementCounter将对计数器进行操作,方法是将计数器减去有效负载参数中存储的值。我们将递减 state.count 的值,直到它达到[T1]。为了确保state.count的值不能小于0,我们将使用三元语句检查它,并相应地设置它的值。

第二个突变resetCounter将计数器的值重置为初始状态:

  mutations: {
    decrementCounter(state, payload) {
      state.count = state.count - payload;
      state.count<0 ? state.count=0 : state.count
    },
    resetCounter(state) {
      state.count = 5;
    }
  },

接下来,我们将设置两个动作,decrementreset

  actions: {
    decrement(state, payload) {
      state.commit("decrementCounter", payload);
    },
    reset(state) {
      state.commit("resetCounter");
    }
  }

最后,我们正在设置我们的应用程序,并在其 Vue 构造函数中指定[T0]、[T1]、[T2]和[T3]选项:

const app = new Vue({
  el: "#app",
  store: store,
  computed: {
    count() {
      return store.getters.counter;
    }
  },
  methods: {
    eatFruit(amount) {
      store.dispatch("decrement", amount);
    },
    counterReset() {
      store.dispatch("reset");
    }
  }
});

接下来,在 HTML 中,我们设置了简单应用程序的结构:

<div id="app">
 <h1>Fruit to eat: {{count}}</h1>
 <button v-on:click="eatFruit(1)">Eat fruit!</button>
 <button v-on:click="counterReset()">Reset the counter</button>
</div>

工作示例可在以下 URL 中找到:https://codepen.io/AjdinImsirovic/pen/aRmENx

使用 Vue DevTools 插件跟踪 Vuex 状态

如果您在 Chrome extensions web store 的搜索字段中键入[T0],您将得到一些结果。第一个结果是官方插件的稳定版本。第二个结果是 Vue DevTools 扩展的测试版。要查看正在开发的所有选项,并查看该插件的发展方向,最好安装测试版。有趣的是,两个版本在 chromedevtools 中打开后显示相同的信息。目前,信息内容为Ready. Detected Vue 2.5.17-beta.0

与普通版相比,实验版增加了几个标签,即routingp``erformance。然而,即使是现有的选项卡也有一些非常有用的改进。例如,Vuex 选项卡能够直接从 DevTools 内部更新状态。要访问该功能,只需按F12键打开 Chrome DevTools 即可。定位 DevTools 以使用 Vue 扩展的最佳方法是将其设置为Dock to bottom选项。按下三个垂直点图标(“T6”)自定义和控制 DevTools(“T7”)图标可访问此选项,该图标位于 DevTools 窗格右上角 DevTools close 图标旁边

一旦启用了“驳到底”功能,Vue 选项卡打开,并且在其中 Vuex 选项卡处于活动状态,您将看到两个窗格。最初,左窗格列出基本状态。这是一个列出所有突变的窗格,允许我们运行时间旅行调试。右窗格列出了实际的负载、状态和突变,因此它为我们提供了一个更细粒度的视图,以了解在任何给定的突变中发生了什么。要对任何特定突变进行时间旅行,只需将鼠标悬停在该突变上,然后单击时间旅行图标。您还可以选择在列出的任何突变上运行CommitRevert。正如您可能猜到的,Commit命令将对当前悬停的突变执行提交,而Revert命令将撤消特定突变的提交。

另一个有用且有趣的功能是能够从 Vuex 选项卡更新状态。例如,假设我们点击Eat fruit!按钮几次。现在,我们可以点击突变窗格中任何给定的decrementCounter突变,我们将在右侧窗格中获得以下信息:

▼ mutation
    payload: 1
    type: ''decrementCounter''
▼ state
    count: 1
▼ getters
    counter: 1

使用此窗格非常简单。如果我们需要更新我们的状态,将鼠标悬停在state条目内的count: 1上会触发四个图标出现:编辑值图标、减号图标、加号图标和复制值图标,显示为三个垂直点。在这里,我们还可以看到getters是只读的证据。将鼠标悬停在getters条目上不会显示任何编辑图标。与此相反,statemutation条目都可以在此窗格中编辑。

改进我们的水果柜台应用程序

在本节中,我们将对水果计数器应用程序进行一些改进。我们的目标是看看如何使用 Vuex 扩展我们的应用程序。

我们将通过添加附加功能来更新我们的应用程序。也就是说,我们将为不同的水果添加按钮:苹果和梨。要吃水果的数量以及吃水果的数量和种类也将出现在我们的应用程序中。

下面是更新的 JS 代码。我们首先定义存储中的状态:

const store = new Vuex.Store({
  state: {
    count: 5,
    apples: 0,
    pears: 0
  },

接下来,我们设置 getter:

  getters: {
    counter(state) {
      return state.count;
    },
    appleCount(state) {
      return state.apples;
    },
    pearCount(state) {
      return state.pears;
    }
  },

定义突变时,我们需要decrementWithApplesCounterdecrementWithPearsCounter以及resetCounter功能:

  mutations: {
    decrementWithApplesCounter(state, payload) {
      state.count = state.count - 1;
      state.count < 0 ? (state.count = 0) : (state.count, state.apples 
       += 1);
    },
    decrementWithPearsCounter(state, payload) {
      state.count = state.count - 1;
      state.count < 0 ? (state.count = 0) : (state.count, state.pears 
      += 1);
    },
    resetCounter(state) {
      state.count = 5;
      state.apples = 0;
      state.pears = 0;
    }
  },

接下来,我们将列出我们的操作,decrementWithApplesdecrementWithPearsreset

  actions: {
     decrementWithApples(state, payload) {
       setTimeout(() => {
         state.commit("decrementWithApplesCounter", payload);
       }, 1000)
     }, 
    decrementWithPears(state, payload) {
      state.commit("decrementWithPearsCounter", payload);
    },
    reset(state) {
      state.commit("resetCounter");
    }
  }
});

最后,我们将添加 Vue 构造函数:

const app = new Vue({
  el: "#app",
  store: store,
  computed: {
    count() {
      return store.getters.counter;
    },
    apples() {
      return store.getters.appleCount;
    },
    pears() {
      return store.getters.pearCount;
    }
  },
  methods: {
    eatApples(payload) {
      store.dispatch("decrementWithApples", payload);
    },
    eatPears(payload) {
      store.dispatch("decrementWithPears", payload);
    },
    counterReset() {
      store.dispatch("reset");
    }
  }
});

正如我们在这段代码中看到的,我们可以在一个 JS 三元组中更新多个变量值。我们还通过setTimeout()函数调用“模拟”对服务器的调用;这是不必要的,但用作更复杂的异步操作的示例。

更新后的 HTML 代码如下所示:

<div id="app" class="p-3">
  <h1>Fruit to eat: {{ count }}</h1>
  <p>Eaten: {{ apples }} apples, {{ pears }} pears</p>
  <button v-on:click="eatApples(1)" class="btn btn-success">
    An apple!
  </button>
  <button v-on:click="eatPears(1)" class="btn btn-warning">
    A pear!
  </button>
  <button v-on:click="counterReset()" class="btn btn-danger">
    Reset the counter
  </button>
</div>

更新后的示例应用程序可在此处找到:https://codepen.io/AjdinImsirovic/pen/EdNaaO

总结

在本章中,我们了解了 Vuex,这是一个功能强大的 Vue 插件,可帮助我们从集中的全局存储中管理状态。我们了解了状态是什么,以及为什么需要在更复杂的应用程序中集中数据存储。我们讨论了单向数据流及其在 Vuex 中的实现,通过使用 getter、存储突变和存储操作。我们从理论转向实践,首先构建一个简单的应用程序,然后学习如何在 Vue Devtools 扩展的帮助下简化开发过程

在下一节中,我们将使用 Vue 路由进行路由,并将研究使用 Nuxt 的服务器端渲染。