Vue 3 實戰:打造高效能、可重複使用的 useFetch 非同步封裝
在現代前端開發中,處理 API 請求不只是拿到數據而已,更包含了 Loading 載入中、Error 錯誤處理 以及 數據響應性。今天我們示範如何結合 Axios 與 Composable,打造一個靈活且具備完整錯誤傳遞機制的 useFetch。
本篇會用到 Axios 模組化 的目錄結構,建議先看過。
核心設計:封裝 Axios 與錯誤向上傳遞
在進行二次封裝時,最容易犯的錯誤就是吃掉錯誤。為了讓呼叫端(如元件)能根據錯誤進行後續處理(例如:顯示通知),我們必須在 Axios 封裝層使用 throw err。
修改 utils/axios/api.js
確保當 API 發生異常時,錯誤能被正確拋出到 Composable 層。
// utils/axios/api.js
import instance from './instance';
export default {
async GET(endPoint, config = {}) {
try {
const res = await instance.get(endPoint, config);
return res.data;
} catch (err) {
console.log('Axios 封裝方法失敗:', err);
throw err; // 重新拋出錯誤給上層處理
// return Promise.reject(err); // 也可以這樣寫
}
},
};錯誤要一路傳到能處理它的那一層
throw err 與 return Promise.reject(err) 在 async 函式中效果相同,重點是 不要在中途把錯誤吞掉。整條路徑的分工是:
- Axios 攔截器:處理所有請求共通的事,例如:401 導回登入。
- api.js:只負責回傳資料,錯誤照原樣往上拋。
- useFetch:把錯誤存進
error,變成畫面可以讀的狀態。 - 元件:決定要顯示紅字、彈窗還是重試按鈕。
實作 useFetch Composable
這個 Composable 的目標是:只要傳入 endPoint,就自動回傳數據、載入狀態與錯誤訊息。
// composables/useFetch.js
import api from '@/utils/axios/api';
import { ref } from 'vue';
export default (endPoint, config = {}) => {
const data = ref(null);
const isLoading = ref(false);
const error = ref(null);
async function loadData() {
isLoading.value = true;
error.value = null;
try {
const res = await api.GET(endPoint, config);
if (res) data.value = res;
} catch (err) {
error.value = err;
} finally {
isLoading.value = false;
}
}
loadData();
return { data, error, isLoading };
};為什麼直接呼叫 loadData(),不等 onMounted?
因為這個請求 不需要 DOM。在 setup 期間就送出,資料會比等到 onMounted 更早回來。只有需要等畫面就緒或使用者操作時,才該延後執行。
在元件中實戰應用:多 API 呼叫的命名藝術
當一個頁面需要同時呼叫多個 API(例如:使用者清單、產品清單)時,使用 ES6 解構賦值(Destructuring)並重新命名 是維持程式碼清晰度的最佳實踐。
// App.vue
<script setup>
import useFetch from "@/composables/useFetch";
const {
data: users,
isLoading: usersIsLoading,
error: usersError,
} = useFetch("/users");
</script>
<template>
<div v-if="!usersIsLoading">
<h2>Users</h2>
<p v-if="usersError" v-text="usersError"></p>
<ul v-else-if="users?.length">
<li v-for="u in users" :key="u.id">
<pre v-text="u"></pre>
</li>
</ul>
<p v-else>No users data.</p>
</div>
<div v-else>Loading...</div>
</template>模板中的三種狀態(載入中、錯誤、有資料)是用 v-if / v-else-if 串起來的,清單本身則交給 v-for 渲染。
加上重新載入與自動重取
原本的版本只在建立時請求一次。實務上還會需要 手動重試 與 參數變動時自動重取,兩者都只是小幅擴充:
// composables/useFetch.js
import api from '@/utils/axios/api';
import { ref, watch, toValue } from 'vue';
export default (endPoint, config = {}) => {
const data = ref(null);
const isLoading = ref(false);
const error = ref(null);
async function loadData() {
isLoading.value = true;
error.value = null;
try {
// toValue 讓 endPoint 可以是字串、ref 或 getter
data.value = await api.GET(toValue(endPoint), config);
} catch (err) {
error.value = err;
} finally {
isLoading.value = false;
}
}
// 來源變動就重新請求,immediate 取代原本手動呼叫的那一行
watch(() => toValue(endPoint), loadData, { immediate: true });
// 把 loadData 一起回傳,元件才能主動重試
return { data, error, isLoading, refetch: loadData };
};有了 toValue 與 watch,就能把 路由參數 直接餵進去,切換商品時自動重新取資料:
<script setup>
import { useRoute } from "vue-router";
import useFetch from "@/composables/useFetch";
const route = useRoute();
const { data: product, refetch } = useFetch(() => `/products/${route.params.id}`);
</script>這也順便解決了 動態路由 那篇提到的元件重用問題:onMounted 不會再次觸發,但 watch 會。
關鍵技術點解析
為什麼要用解構重命名?
在 useFetch 中,回傳的屬性名稱通常是固定的 data、isLoading、error。但在大型專案中,您會在同一個元件呼叫多個 Composable。透過 data: users 這種寫法,不僅解決了命名衝突,也增加了程式碼的 語意化。
錯誤處理的正確姿勢
許多開發者會在 Composable 裡直接 console.log 錯誤後就結束了。但正確的做法是更新 error 響應式變數,讓 UI 能夠根據錯誤狀態顯示對應的視窗或文字說明,這對使用者體驗(UX)至關重要。
什麼時候該改用 Pinia?
useFetch 每次呼叫都是 獨立的一次請求,兩個元件各自呼叫就會各打一次 API。若這份資料需要跨頁面共用或做快取,請把請求寫進 Pinia 的 action,由 store 保存唯一的一份。
常見問題
為什麼 Axios 封裝層要 throw err?
若在封裝層把錯誤吃掉,上層只會拿到 undefined 卻不知道失敗,畫面無法顯示錯誤狀態。把錯誤往上拋,才能由 Composable 更新 error 讓 UI 呈現。
useFetch 為什麼可以直接在函式裡就發送請求?
因為它是在 setup 中同步呼叫的,此時發出請求比等到 onMounted 更早,資料能更快回來。若請求需要等待 DOM 或使用者操作,才需要延後執行。
為什麼要用解構重新命名?
因為回傳的名稱固定是 data、isLoading、error,同一個元件呼叫多次就會衝突。寫成 data: users 既避開衝突,也讓變數更有語意。
要怎麼手動重新載入資料?
把內部的 loadData 一起回傳出去,命名為 refetch 之類的名稱,元件就能在按下重試按鈕或新增資料成功後主動呼叫它。
網址參數改變時要怎麼自動重新取得?
讓 useFetch 接受 ref 或 getter 作為網址來源,並在內部用 watch 搭配 immediate 監聽它。這樣 路由參數 變動時就會自動重新請求,不必自己呼叫。
useFetch 與 Pinia 該怎麼分工?
useFetch 每次呼叫都是獨立的一次請求,適合單一頁面自己用的資料;若資料要跨頁面共用或需要快取,把請求寫進 Pinia 的 action 更合適。