Skip to content

Vue 3 實戰:打造高效能、可重複使用的 useFetch 非同步封裝

Vue 3 非同步資料取得:useFetch Composable

在現代前端開發中,處理 API 請求不只是拿到數據而已,更包含了 Loading 載入中Error 錯誤處理 以及 數據響應性。今天我們示範如何結合 Axios 與 Composable,打造一個靈活且具備完整錯誤傳遞機制的 useFetch

本篇會用到 Axios 模組化 的目錄結構,建議先看過。

核心設計:封裝 Axios 與錯誤向上傳遞

在進行二次封裝時,最容易犯的錯誤就是吃掉錯誤。為了讓呼叫端(如元件)能根據錯誤進行後續處理(例如:顯示通知),我們必須在 Axios 封裝層使用 throw err

修改 utils/axios/api.js

確保當 API 發生異常時,錯誤能被正確拋出到 Composable 層。

js
// 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 errreturn Promise.reject(err)async 函式中效果相同,重點是 不要在中途把錯誤吞掉。整條路徑的分工是:

  • Axios 攔截器:處理所有請求共通的事,例如:401 導回登入。
  • api.js:只負責回傳資料,錯誤照原樣往上拋。
  • useFetch:把錯誤存進 error,變成畫面可以讀的狀態。
  • 元件:決定要顯示紅字、彈窗還是重試按鈕。

實作 useFetch Composable

這個 Composable 的目標是:只要傳入 endPoint,就自動回傳數據、載入狀態與錯誤訊息。

js
// 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)並重新命名 是維持程式碼清晰度的最佳實踐。

vue
// 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 渲染。

加上重新載入與自動重取

原本的版本只在建立時請求一次。實務上還會需要 手動重試參數變動時自動重取,兩者都只是小幅擴充:

js
// 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 };
};

有了 toValuewatch,就能把 路由參數 直接餵進去,切換商品時自動重新取資料:

vue
<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 中,回傳的屬性名稱通常是固定的 dataisLoadingerror。但在大型專案中,您會在同一個元件呼叫多個 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 或使用者操作,才需要延後執行。

為什麼要用解構重新命名?

因為回傳的名稱固定是 dataisLoadingerror,同一個元件呼叫多次就會衝突。寫成 data: users 既避開衝突,也讓變數更有語意。

要怎麼手動重新載入資料?

把內部的 loadData 一起回傳出去,命名為 refetch 之類的名稱,元件就能在按下重試按鈕或新增資料成功後主動呼叫它。

網址參數改變時要怎麼自動重新取得?

useFetch 接受 ref 或 getter 作為網址來源,並在內部用 watch 搭配 immediate 監聽它。這樣 路由參數 變動時就會自動重新請求,不必自己呼叫。

useFetch 與 Pinia 該怎麼分工?

useFetch 每次呼叫都是獨立的一次請求,適合單一頁面自己用的資料;若資料要跨頁面共用或需要快取,把請求寫進 Pinia 的 action 更合適。

延伸閱讀