Skip to content

以简单的方式管理 Vue.js 中请求数据的状态

Published:

前言

获取和显示数据对于前端工程师来说是家常便饭的任务,但随着背后状态的增多,整个项目可能变得非常混乱。

从发送一个简单的请求开始

我们有一个简单的产品 API,需求是获取数据并显示在页面上。我们使用 JS 原生的 fetch APIAsync/Await 处理异步请求。

<script setup>
  import { ref } from "vue";

  const product = ref({});

  async function getProduct() {
    const productResponse = await fetch("https://dummyjson.com/products/1");
    if (!productResponse.ok) {
      console.error(productResponse);
      return;
    }
    const productJSON = await productResponse.json();
    product.value = productJSON;
  }

  getProduct();
</script>

<template>
  <div>
    <img :src="product.thumbnail" />
    <h2>{{ product.title }}</h2>
    <p>{{ product.description }}</p>
  </div> </template
>​​

image.png

虽然这个请求能够正确地返回数据,但有个问题:**用户无法得知请求是否出错或正在加载。**接下来,我们将通过新增状态来解决这个问题。

新增加载与错误状态

在这个版本中,我们添加了 isLoadingerrorMessage 状态,用来记录请求的状态,并通过这些状态来给用户更多的提示。

<script setup>
  import { ref } from "vue";

  const { productUrl } = defineProps({
    productUrl: {
      type: String,
      default: "https://dummyjson.com/products/1",
    },
  });

  const product = ref({});
  const isLoading = ref(true);
  const errorMessage = ref("");

  getProduct(productUrl);

  async function getProduct(productUrl) {
    isLoading.value = true;
    const productResponse = await fetch(productUrl);
    if (!productResponse.ok) {
      console.error(productResponse);
      errorMessage.value = (await productResponse.json()).message;
      return;
    }
    const productJSON = await productResponse.json();
    product.value = productJSON;
    isLoading.value = false;
  }
</script>

<template>
  <div id="app">
    <div class="mx-auto max-w-fit bg-white p-4 text-black">
      <div class="bg-red-300 p-4" v-if="errorMessage">{{ errorMessage }}</div>
      <!-- 在这里判断是否请求中 -->
      <div v-else-if="!isLoading">
        <img :src="product.thumbnail" />
        <h2>{{ product.title }}</h2>
        <p>{{ product.description }}</p>
      </div>
      <div v-else>Loading...</div>
    </div>
  </div> </template
>​​

i

到这里用户使用体验已经接近完善了,但开发体验却不尽人意,因为当需求变更时,组件终将会塞满各式各样的状态,光是理解这些状态避免切换错误就是一件很累人且容易出错的事。

除此之外加载过程的版面偏移造成的闪烁除了影响用户体验之外,重新计算页面布局也会造成性能上的问题,下一版本来尝试制作 UI 并解决这个问题。

添加 UI 与骨架屏

这次版本除了给数据添加基本样式之外也新增了骨架屏幕,其实就是更贴近实际结果更华丽的 Loader 而已,这么做的好处是可以解决先前遇到的版面偏移。

<template>
  <div v-if="errorMessage">{{ errorMessage }}</div>
  <div v-else-if="!isLoading">
    <ProductCard :product="product" />
  </div>
  <div v-else>
    <SkeletonCard />
  </div>
</template>

到这个步骤,用户体验已非常完美,既有数据的展示,也有加载以及错误状态,但在状态管理方面则可以考虑以下几种方案来增进开发体验。

一种方式:通过 Composable 包装请求逻辑

反思处理这些状态的过程,发现其实这些状态都是为了处理数据的请求,因此可以通过封装相关逻辑来处理这些状态,让组件只需要关注数据本身即可。

举例来说,制作一个 useFetch 并输入请求 URL,然后输出数据、错误信息、请求状态等,不用再为每个请求开关状态。

<script setup>
  const { data, error } = useFetch("https://dummyjson.com/products/1");
</script>

<template>
  <div v-if="error">{{ error.message }}</div>
  <div v-else-if="product">
    <ProductCard :product="product" />
  </div>
  <div v-else>
    <!-- 插入 4 个骨架屏卡片 -->
    <SkeletonCard v-for="i in 4" :key="i" />
  </div>
</template>

具体实例可以参考官方文档Vueuse 的示例或是 Nuxt 的示例,在这之上可以扩展更多功能像是快取、重试(Refresh)、渲染模式(mode)切換……等进阶功能。

另一种方式:使用实验性组件 Suspense

<Suspense>​ 实际上就是 Vue.js 的默认组件用于处理异步加载的组件。在异步组件加载完成之前,可以显示默认内容。

<Suspense>​ 有两个插槽,分别是 defaultfallback。它们的用途也很明显:default 用于放入异步组件,fallback 则是放入默认组件(如加载提示等信息)。

<template>
  <!-- Vue.js 内置組件不需要引入,可以直接在 Template 中使用 -->
  <Suspense>
    <template #default>
      <!-- 放入异步组件 -->
      <AsyncProductCard />
    </template>
    <template #fallback>
      <SkeletonCard />
    </template>
  </Suspense>
</template>

所谓的异步组件实际上有两种可能性:1. async setup()​ 或者 2. Top level await

<!-- async setup() -->
<script>
  export default {
    async setup() {},
  };
</script>

<!-- top level await -->
<script setup>
  await xxx();
</script>

所以这样我们可以直截了当地制作一个异步组件 AsyncProductCard​(如下),并且通过上一层的 <Suspense>​ 帮助我们在组件加载完成之前自动显示预设内容。

<script setup>
  import ProductCard from '../ProductCard.vue';

  import { ref } from 'vue';

  const product = ref({});

  async function getProduct() {
    const productResponse = await fetch('https://dummyjson.com/products/1');
    if (!productResponse.ok) {
      console.error(productResponse);
      // 在请求失败时抛出错误让父组件处理
      throw Error('请求失败');
    }
    const productJSON = await productResponse.json();
    product.value = productJSON;
  }
  await getProduct();
</script>

<template>
  <ProductCard :product="product" />
</template>

至于错误处理可以使用 Vue.js@3onErrorCaptured​ 生命周期,这个生命周期可以捕捉子组件的错误,并且可以在上层组件中处理错误,这样一来请求错误也可以显示反馈给用户了。

<script setup>
  import { ref } from "vue";

  const error = ref(null);
  onErrorCaptured(err => {
    error.value = err.message;
  });
</script>

<template>
  <div v-if="error" class="bg-red-300">Err: {{ error }}</div>
  <ProductCard :product="product" />
</template>

参考资料