背景
索取並顯示資料對前端工程師來說是家常便飯的事,但隨著背後延伸的狀態越來越多,就會讓整個專案極度混亂,因此想要藉由這篇文章點出現有問題並提出一些可行的方案,紀錄尋找更高效解法的過程。
雖然文章中使用的是 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 實作,在這之上可以擴充更多功能像是快取、重試、渲染模式切換……等進階功能。
<Suspense>
元件
Lv.3-2:使用實驗性 <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 這個檔案之中,歡迎自由索取練習。