十四、使用 Vuex 和发送 GET HTTP 请求来简化状态管理

本章是关于发送 HTTP 请求和解决大型 web 应用中最常见的问题——同步一个组件与另一个组件的状态的问题。 在大型和复杂的应用中,您需要一个工具来集中应用的状态,并使数据流透明和可预测。

在本章中,我们将涵盖以下主题:

  • 理解复杂的状态管理
  • 发送 HTTP 请求
  • 使用 Vuex 设置状态管理

技术要求

你需要 Visual Studio Code 来完成本章。

本章已完成的知识库可以在https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter14找到。

了解复杂状态管理

在开发大型复杂的 Angular、React 或 Vue web 应用时,你会遇到状态管理。 那么状态管理是什么意思呢?

应用状态管理是当你的应用从一个或几个视图开始成长。 您可能会开始遇到这样的问题:您希望在不同的嵌套组件之间共享应用的一些状态。 举个例子,你必须创建一个机制,让两个深度组件始终同步。 见图 14.1图:

Figure 14.1 – Shared component's local state

图 14.1 -共享组件的本地状态

图 14.1显示组件 Y组件 X状态相同。 有很多方法可以做到。 一种是绕过事件,从顶级组件传入属性。 尽管您可以传递道具和事件,但在多个嵌套组件中传递事件和道具会使应用难以维护,也会使代码难以推理。

了解全局状态

那么,对于由于多个嵌套组件之间的状态共享而导致的不可维护的代码,该如何解决呢? 应用范围状态或全局状态,也称为,即存储。 全局状态解决方案的思想是,每个视图和每个组件都可以只是一个中心状态的反映。 见图 14.2:

Figure 14.2 – Reactive global state

图 14.2 -无响应的全局状态

图 14.2 显示了一个活跃的全局状态,它可以被任何组件访问。 拥有一家商店是件好事。 我们只有一个真理的来源,而一切都只是它的反映。 全局状态或存储的活动性是确保用户在应用中看到的所有内容始终保持同步的好方法。

那么如何在 Vue.js 中建立一个商店呢? 我们稍后再做,但让我们在下一节中做一些不带存储的简单数据获取。 简单的数据获取将证明我们可以向我们的 ASP 发送一个 HTTP GET 请求.NET Core Web API。

在 Vue.js 中发送 HTTP 请求

如果您正在开发现代 web 应用,那么将 HTTP 请求发送到 RESTFul 服务是很简单的。 HTTP 客户端库有api-saucesuper-agentaxios。 你知道 JavaScript 本身有一个用于发送 HTTP 请求的本地 API 吗? 是的,这是 Fetch API。

Fetch API 可以在 JavaScript 和 TypeScript 中使用。 然而,它只在现代浏览器中工作,当您在旧浏览器中加载应用时将不起作用。 不仅如此,在我看来,Fetch API 在使用它的 API 发送请求时也可能过于冗长。 我更喜欢一个带有抽象的 HTTP 客户端库,而不是编写额外的代码,比如res.json()headersproperty方法。 在这种情况下,我们将使用 Axios 作为 HTTP 客户端库。

Axios 是一个基于承诺的 HTTP 客户端库,用于浏览器和服务器端 Node.js 应用。它使用简单,也支持旧浏览器。 我们将把 Axios 和 Day.js 一起安装,这是一个用于操作日期和时间的库。

因此,转到您的vue-app根目录并运行以下npm命令:

npm i axios dayjs

前面的npm命令将安装axiosdayjs

接下来,我们在src目录中创建一个名为api的文件夹。

api文件夹中创建一个 JavaScript 文件api-v1-config.js,并编写以下代码:

import axios from "axios";
const debug = process.env.NODE_ENV !== "production";
const baseURL = debug
  ? "https://localhost:5001/api/v1.0/"
  : "https://traveltour.io/api/v1.0/";
let api = axios.create({ baseURL });
export default api;

前面的代码是针对 ASP 的 API 版本之一的 Axios 设置.NET Core Web API。 我们正在创建一个 Axios 实例,并将一个基 URL 传递给它使用。

让我们在api文件夹中创建另一个 JavaScript 文件api-v2-config.js,并编写如下代码:

import axios from "axios";
const debug = process.env.NODE_ENV !== "production";
const baseURL = debug
? "https://localhost:5001/api/v2.0/"
: "https://traveltour.io/api/v2.0/";
let api = axios.create({ baseURL });
export default api;

前面的代码是针对我们的 ASP 的 API 版本的一个 Axios 配置.NET Core Web API。 你会注意到这里唯一不同的是版本。

api文件夹中创建另一个 JavaScript 文件weather-forecast-services.js,并编写以下代码:

import api from "@/api/api-v2-config";
export async function getWeatherForecastV2Axios(city) {
  return await api.post(`WeatherForecast/?city=${city}`);
}

前面的代码是用于向WeatherForecast v2控制器发送 POST 请求的服务。 我们将 Axios 配置的默认导出命名为api,然后使用它调用WeatherForecast端点的POST方法。 我喜欢在函数服务中添加axios后缀,以便在阅读 IDE 或代码编辑器的智能感知时帮助我识别要导入的正确文件。

现在让我们更新views|AdminDashboard文件夹的WeatherForecast.vue组件。 用以下代码更新文件夹的内容:

<script>
import { getWeatherForecastV2Axios } from "@/api/weather-
  forecast-services";
export default {
  name: "WeatherForecast",
  async mounted() {
    await getWeatherForecastV2Axios("Oslo");
  },
};
</script>

在前面的代码中,我们导入了getWeatherForecastV2Axios服务,并在mounted()生命周期钩子中使用它,以便在 DOM 呈现之前提前触发它。

通过在Travel.WebApi项目中运行以下命令来运行后端应用和前端应用:

dotnet run

运行前面的dotnet命令后等待几秒钟,然后进入浏览器,输入https://localhost:5001,检查 Chrome 的 DevTool,如下截图所示:

Figure 14.3 – Chrome DevTool status after sending the POST request to the WeatherForecast API

图 14.3 -向天气预报 API 发送 POST 请求后 Chrome DevTool 的状态

14.3POST 请求返回一个【T6 状态】200 OK 代码,这意味着我们得到我们所要求的,那就是下面的 JSON 结果:****

Figure 14.4 – JSON response after sending the POST request to the WeatherForecast API

图 14.4 -向天气预报 API 发送 POST 请求后的 JSON 响应

WeatherForecast控制器的 JSON 响应如图图 14.4所示。 响应表示我们可以发送请求并从后端获得响应。

现在我们知道浏览器中有数据可以用于用户界面,让我们为数据构建一个 UI。

让我们再次更新WeatherForecast.vue

让我们从我们安装的dayjs库中导入relativeTimedayjs:

import relativeTime from "dayjs/plugin/relativeTime";
import dayjs from "dayjs";

relativeTime将日期格式化为相对时间字符串,如an hour agoin 2 days

现在让我们更新挂载的生命周期钩子:

  async mounted() {
    this.loading = true;
    dayjs.extend(relativeTime);
    await this.fetchWeatherForecast(this.selectedCity);
    this.loading = false;
  },

我们使用库dayjs来操作日期。 您还会注意到,我们正在使用本地状态loading,将其设置为 true,然后在获取数据后将其设置为 false。 我们将使用本地状态loading在应用的屏幕上显示一个旋转器组件。

现在让我们定义我们的局部状态:

  data() {
    return {
      weatherForecast: [],
      cities: [],
      selectedCity: "Oslo",
      loading: false,
    };
  },

我们在本地状态中有一个weatherForecast数组、一个cities数组、一个selectedCity字符串和一个loading布尔值。 它们都是用默认值初始化的。

让我们在methods对象中添加一个异步方法,如下所示:

  methods: {
    async fetchWeatherForecast(city = "Oslo") {
      this.loading = true;
      try {
        const { data } = await getWeatherForecastV2Axios(
          city);
        console.log(data);
        this.weatherForecast = data?.map((w) => {
          const formattedData = { ...w };
          let date = w.date;
          formattedData.date = dayjs(date).fromNow();
          return formattedData;
        });
      } catch (e) {
        alert("Something happened. Please try again.");
      } finally {
        this.loading = false;
      }
    },
  },

我们在这个函数中将旋转器loading设置为true,使用getWeatherForecastV2Axios服务发送请求。 在获得数据之后,我们将使用 JavaScript 数组实用程序map,它就像一个循环,从 JSON 数组响应格式化每个对象的日期。

让我们还创建另一个方法来返回颜色编码的温度。 在 GitHub repo 中获得完整的函数,因为它太大了,无法在这里写入:

  getColor(summary) {
      switch (summary) {
        case "Freezing":
          return "indigo";
        // get the rest from the github
        default:
          return "grey";
      }
    },

该方法是本质上只是一个助手,根据温度返回颜色。

现在来看一下template语法。 这里是:

<template>
  <v-container>
    <div class="text-h4 mb-10">
      Two-week weather forecast of different cities
    </div>
    <div class="v-picker--full-width d-flex justify-center"
       v-if="loading">
      <v-progress-circular
        :size="70"
        :width="7"
        color="purple"
        indeterminate
      ></v-progress-circular>
    </div>
    <!-- Insert <v-simple-table> here -->
  </v-container>
</template>

前面的代码是应用的titlecontainerWeatherForecast页面的微调器 UI。

现在将v-simple-table组件包含在WeatherForecastUI 的容器中。 将组件插入到你会找到<!-- Insert <v-simple-table> here -->注释的地方:

    <v-simple-table>
      <template v-slot:default>
        <thead>
          <tr>
            <th class="text-left">Dates</th>
            <th class="text-left">City</th>
            <th class="text-left">&#8451;</th>
            <th class="text-left">&#8457;</th>
            <th class="text-left">Summary</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="item in weatherForecast" 
            :key="item.date">
            <td>{{ item.date }}</td>
            <td>{{ item.city }}</td>
            <td>{{ item.temperatureC }}</td>
            <td>{{ item.temperatureF }}</td>
            <td>
              <v-chip :color="getColor(item.summary)" dark>{{
                item.summary
              }}</v-chip>
            </td>
          </tr>
        </tbody>
      </template>
    </v-simple-table>

其中的v-simple-table项为WeatherForecast表,其中显示了奥斯陆某一特定日期的温度:

Figure 14.5 – Two-week weather forecast

图 14.5 -两周天气预报

以上预报为Oslo的两周预报。 在那里你可以看到日期、城市、摄氏度和华氏温度,以及天气的可视化总结。

在下一节中,我们将添加一个下拉选择器,我们可以在其中选择一个城市,我们可以选择的城市将来自我们的 ASP.NET Core Web API。

现在我们有了一个使用简单 HTTP 请求而不使用存储的WeatherForecast特性。 我们将在下一节中构建的特性将使用一个 Vuex 状态管理库。 我们将在另一个组件重用的功能,需要一个存储在下一章,第 15 章,发帖子,删除,把 HTTP 请求在 Vue.js Vuex【显示】。

使用 Vuex 设置状态管理

Vuex是 Vue.js 的官方状态管理库,广泛用于管理复杂组件。 Vuex 有一个被动的全球商店,设置起来相当容易。 在编写代码时,我将解释 Vuex 实现的各个部分。

但是在我们开始构建我们的存储之前,让我们先从 API 控制器中删除授权,这样我们在向api/v1.o/端点发送请求时就不需要认证令牌了。

为此,转到Travel.WebApi项目的Travel.WebApi.Controllers.v1名称空间中的ApiController.cs文件,并注释Authorize属性,如下所示:

// [Authorize]

在注释了Authorize属性之后,我们现在可以暂时使用api/v1.0/

让我们从设置 Vuex 的更新部分开始。

第一步-写一个商店

store文件夹中创建一个名为tour的文件夹。 它将像这样:src|store|tour

第二步-编写模块

tour文件夹中创建一个名为services.js的 JavaScript 文件,并编写如下代码:

import api from "@/api/api-v1-config";
export async function getTourListsAxios() {
  return await api.get("TourLists");
}

我们正在创建一个向版本 1TourLists控制器或端点发送请求的服务。

第 3 步-如果我们使用 TypeScript,编写一个模块

创建一个名为types.js的 JavaScript 文件,仍然在tour文件夹中,然后编写以下代码:

export const LOADING_TOUR = "LOADING_TOUR";
export const GET_TOUR_LISTS = "GET_TOUR_LISTS";

前面的代码不是编写 Vuex 时的必要条件,而是在实现状态管理时成为设计的一部分。 前面的类型是,称为动作类型,我们访问这些类型是为了让存储知道它在接收到动作后应该在状态中采取什么样的动作。 蛇形的类型只是字符串,但是它们可以在写入操作时防止拼写错误,稍后您将看到。

第 4 步-编写 API 服务

tour文件夹中创建一个名为actions.js的 JavaScript 文件,然后编写如下代码:

import * as types from "./types";
import { getTourListsAxios } from "@/store/tour/services";
// asynchronous action using Axios
export async function getTourListsAction({ commit }) {
  commit(types.LOADING_TOUR, true);
  try {
    const { data } = await getTourListsAxios();
    commit(types.GET_TOUR_LISTS, data.lists);
  } catch (e) {
    alert(e);
    console.log(e);
  }
  commit(types.LOADING_TOUR, false);
}

该操作包含如何更新存储的全局状态的说明。 Action 是在其他 JavaScript 框架的状态管理库中也可以找到的术语。 因此,记住这一点很有价值。

此外,如果需要,操作可以包含异步操作。 我们定义了一个带有解构的commit参数的action函数来进行突变。

要使用提交,只需将操作类型作为第一个参数传递。 如果在特定的提交中需要一个有效载荷,我们可以使用第二个可选参数来传递有效载荷。 一个例子就是commit(types.LOADING_TOUR, true);。 在前面的句子中,这行代码是一个关于用布尔参数作为动作的payload加载tourcommit语句。

我们并不局限于在一个action函数中编写一个提交。 只要需要,我们可以编写一个或多个提交。

我们在actions.jsgetTourListAction中所做的是启用loading tour。 我们的目标是在使用getTourListsAxios服务获取数据时创建一个微调器。 然后,我们将使用commit(types.GET_TOUR_LISTS, data.lists)中的响应将数据保持在全局状态。

然后通过falseloading tour停止纺纱器的旋转。

在后面的章节中,当我们写更多的动作时,你会看到更多。

第五步-写一个动作类型

tour文件夹中创建一个名为state.js的 JavaScript 文件,然后编写如下代码:

const state = {
  lists: [],
  loading: false,
};
export default state;

state对象将成为应用的全局状态的一部分,也就是商店。 存储是一个具有默认值或初始值的属性的大对象。 您必须记住初始值或初始状态,因为它们也适用于不同 JavaScript 框架中的任何其他状态管理库。

第六步-写一个动作

tour文件夹中创建另一个名为mutations.js的 JavaScript 文件,然后编写如下代码:

import * as types from "./types";
const mutations = {
  [types.GET_TOUR_LISTS](state, lists) {
    state.lists = lists;
  },
  [types.LOADING_TOUR](state, value) {
    state.loading = value;
  },
};
export default mutations;

突变是函数,根据被分派的动作的类型获得触发器。 它们执行实际的状态修改。 正如您在前面的代码中看到的,types.GET_TOUR_LISTS更改了state.lists的值。 参数state是第一个参数,它是全局状态的一部分。 它会自动被维克斯通过。

第二个参数是来自我们前面定义的操作的可选负载。

另外,突变的工作是直接更新全局状态的一部分。 这取决于是否要删除或更新现有状态。

步骤 7 -写一个状态

tour文件夹中创建一个名为getters.js的 JavaScript 文件,然后编写如下代码:

const getters = {
  lists: (state) => state.lists,
  loading: (state) => state.loading,
};
export default getters;

getter是用于存储的计算属性或派生值。 getters的结果或输出基于其依赖项进行缓存,当其任何依赖项发生更改时将重新运行或重新计算。 我们将在组件中包含getters,以便将全局状态连接到 UI。

步骤 8 -写一个突变

tour文件夹中创建一个名为index.js的 JavaScript 文件。 这个文件将在我们的tour模块的索引文件中。 文件创建后,我们编写以下代码:

import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import * as actions from "./actions";
export default {
  namespaced: true,
  getters,
  mutations,
  actions,
  state,
};

我们导入tour模块或名称空间的index文件的state文件、getters文件、mutations文件和actions文件。 在导入必要的文件后,我们将导出它们和,其中包括一个namespaced属性设置为true

模块或名称空间是存储中的分区。 每个模块或名称空间都有其actionsstatemutationsgetters,甚至还有嵌套模块。

第 9 步-写一个 getter

用下面的代码更新存储的index.js文件。 代码是我们将要添加tour模块的地方:

import Vue from "vue";
import Vuex from "vuex";
import createLogger from "vuex/dist/logger";
import tourModule from "./tour";
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== "production";
const plugins = debug ? [createLogger({})] : [];
export default new Vuex.Store({
  modules: {
    tourModule,
  },
  plugins,
});

我们导入一个记录器,前面文件中的 tour 模块,然后删除以下属性:statemutationsactions。 然后我们使用tourModule作为modules对象的属性。

modules对象是 Vue.js 应用中所有模块的位置。 这意味着当我们在 Vue.js 应用中添加新模块或名称空间时,我们将更新modules对象。

步骤 10 -通过插入模块更新存储

现在让我们更新WeatherForecast.vue页面。 同样,让我们从vuex中导入mapActionsmapGetters:

import { mapActions, mapGetters } from "vuex";

mapGetters是一个助手,它简单地将本地计算属性映射到存储getters。 类似地,mapActions是一个助手,它将存储分派映射到组件方法。 我们将在methods块和computed块中使用这些映射器。

插入mapActionsmethods块的第一行中。 使用spread操作符,以mapActions为前缀的三个圆点如下:

methods: {
    ...mapActions("tourModule", ["getTourListsAction"]),
    …
  },

mapActions将自动创建的local方法this.getTourListsAction映射到this.$store.dispatch("getTourListsAction")。 我们现在可以使用this.getTourListsAction来触发突变的动作。

在使用getTourListsAction之前,我们还希望访问存储的lists状态。 要做到这一点,创建computed块,并使用扩展操作符插入mapGetters,如下所示:

  computed: {
    ...mapGetters("tourModule", {
      lists: "lists",
    }),
  },

mapGetters将自动创建的本地状态this.lists映射到this.$store.getters.lists

第 11 步-使用 mapgetter 和 mapActions 更新组件

让我们用下面的代码来更新方法。 我们将添加两行新代码:

  async mounted() {
    …,
    await this.getTourListsAction();
    this.cities = this.lists.map((pl) => pl.city);
  },

我们触发getTourListsAction,然后从列表中提取城市,并将它们存储在城市的本地状态中。

现在,对于WeatherForecast屏幕的下拉框或选择UI,在template语法区域的v-simple-table组件上添加v-select组件:

    <v-select
      @change="fetchWeatherForecast"
      v-model="selectedCity"
      :items="cities"
      label="City"
      persistent-hint
      return-object
      single-line
      clearable
    ></v-select>

前面的组件将为WeatherForecast屏幕提供功能,我们可以在其中选择查看哪个城市的 14 天天气预报。

在运行 ASP.NET Core 和 Vue.js 应用,我想让你使用的 SQLite 数据库的Travel.WebApi项目。 我已经更新了其中的数据,以帮助我们了解我们在前端的设计方面正在做什么。 您将看到 JSON 相当于 SQLite 数据在 GitHub 回购https://github.com/PacktPublishing/ASP.NET-Core-5-and-Vue.js-3/tree/master/Chapter-14,让你知道日期目前在数据库中,它的形状。

删除项目中现有的TravelTourDatabase.sqlite3数据库文件,并将其替换为 GitHub 存储库中的文件。

通过在Travel.WebApi项目中运行以下命令重新运行应用:

dotnet run

等待几秒钟,然后检查浏览器。 你应该看到下拉菜单选择器如下图所示:

Figure 14.6 – Vuetify select component

图 14.6 - Vuetify 选择组件

图 14.6 显示了选择器,默认的Oslo作为所选城市。 尝试点击它来查看其他的可用城市。

接下来,检查浏览器的 DevTools 上的控制台日志。 您还应该看到我们在商店设置中包含的 Vuex 记录器插件。 它看起来类似于图 14.7 所示:

Figure 14.7 – Vuex logger

Figure 14.7 – Vuex logger

前面截图中的 Vuex 记录器显示了 Vue.js 应用的 Vuex 状态管理中发生的事情的日志。 如果在使用 Vuex 商店的部件时应用中出现意外行为,我们可以使用它来调试应用。

在其他框架中使用 Vuex 和其他状态管理库需要大量代码来设置它们。 但是一旦你完成了所有移动部分的设置和你的商店的配置,添加新的动作和使用它们就变得很容易了,因为你只需要更新现有的代码。 您只需要编写一次设置,其余的都是无缝的。

尽管完成设置需要时间,但是不必在嵌套组件内外传递道具和事件的好处非常好,这将使您在开发嵌套组件之间的复杂状态同步时更加轻松。

我建议记住前面的主题流,无论何时在项目中实现 Vuex,都要有一个清单,说明要做什么。 现在让我们回顾一下你在本章所学的内容。

总结

由于状态管理的概念,本章可能是迄今为止最具挑战性的章节之一。 然而,学习如何进行状态管理是非常宝贵的。 您已经学习了如何使用 Axios 发送 HTTP 请求。 您已经在大型应用中发现了状态管理的思想。

您还学习了如何使用 Vuex 和 Vuex 的部分,例如存储、模块、操作、突变和 getter。

在下一章中,我们将构建在 Vue.js 中使用 Vuex 发送POSTDELETE、PUT HTTP请求的功能。