背景
索取并显示数据对前端工程师来讲是家常便饭,但项目复杂化的状态管理往往导致混乱。希望通过这篇文章提出解决方案和记录过程。
虽然文章中使用的是 Vue Composition API,不过重点不在于使用的框架而是背后的概念。像是 React 的脉络也是类似的,如果你想从 React 的角度切入推荐这篇文章:为什么你不应该在 React 中直接使用 useEffect 从 API 获取数据
Lv.1:从发送一个简单的请求开始
目前有一个简单的产品 API,需求是把数据索取下来并且显示在画面上,这里使用 JS 原生的 fetch API + Async / 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>
1️⃣ 检讨
需求达成了,但你发现这么做并没有办法显示请求出错或是加载的状态,用户无法知道任何进展!因此下一版本来尝试新增更多状态来处理这个问题。
Lv.2:新增加载与错误状态
在这个版本使用更多状态像是 isLoading
或 errorMessage
来记录请求的状态,并且通过这些状态来驱动 UI 给用户更多提示。
<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>
2️⃣ 检讨
到这里用户使用体验已经接近完善了,但开发体验却不尽人意,因为当规模扩大或需求变更时,组件终将会塞满各式各样的状态,光是理解这些状态避免切换错误就是一件很累人且容易出错的事。除此之外加载过程的版面偏移造成的闪烁除了影响用户体验之外,重新计算页面布局也会造成性能上的问题,下一版本来尝试制作 UI 并解决这个问题。
Lv.3:添加 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>
3️⃣ 检讨
到这个步骤,用户体验已非常完美,但在状态管理方面则可以考虑以下几种方案来增进开发体验。
Lv.3-1:通过组合式函数(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> <SkeletonCard /> </div></template>
细节实现可以参考官方案例、 VueUse 的实现或是 Nuxt 实现,在这之上可以扩充更多功能像是缓存、重试、渲染模式切换……等进阶功能。
Lv.3-2:使用实验性 <Suspense>
组件
<Suspense>
实际上就是一个「Vue 的默认组件用于处理异步加载的组件」,可以在异步组件加载完成之前显示默认内容,<Suspense>
会接受两个插槽分别是 default
与 fallback
,它们用途也很明显:default
用于放入异步组件,fallback
则是放入默认组件(加载告示之类的讯息)。
<template> <!-- 默认组件不需要引入,可以直接在 Template 当中使用 --> <Suspense> <template #default> <!-- 放入异步组件 --> <AsyncProductCard /> </template> <template #fallback> <SkeletonCard /> </template> </Suspense></template>
所谓的异步组件其实就是两种可能:1. async setup()
或是 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>
至于错误处理可以使用 Vue3 的 onErrorCaptured 生命周期,这个生命周期可以捕捉子组件的错误,并且可以在上层组件中处理错误,如此一来请求错误也可以显示反馈给用户了。
<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></template>
总结

文章循序渐进的展示如何在 Vue 中实践改良索取数据的用户与开发体验,有将过程记录在 Vue-Fetch-Demo 这个文件之中,欢迎自由索取练习。