本文 AI 產出,尚未審核

Vue3 非同步與資料請求:Fetch 與 Axios 整合教學


簡介

在單頁應用 (SPA) 中,資料的取得與送出是最常見的非同步工作。Vue3 內建的 Composition API 讓我們可以更彈性地管理這些非同步流程,但要真正發揮效能,必須了解底層的 HTTP 請求工具——fetchaxios
本篇文章將說明兩者的差異、如何在 Vue3 中優雅地整合,並提供實作範例,讓你在開發過程中能快速、可靠地與後端 API 溝通。


核心概念

1. 為什麼同時保留 fetchaxios

fetch axios
內建支援 瀏覽器原生 API,無需額外套件 需要安裝 axios 套件
語法 基於 Promise,較為「低階」 包裝好的 Promise,支援攔截器、取消請求等功能
錯誤處理 只會在網路錯誤時 reject,HTTP 錯誤需要自行判斷 response.ok 只要 HTTP 狀態碼非 2xx 就自動 reject
跨平台 可在 Service Worker、Node (v18+) 使用 同樣支援 Node,且有更多設定選項

結論:在小型或簡單的請求時,fetch 足以應付;若需要 攔截器、全域設定、請求/回應轉型axios 會更省事。實務上,許多專案會同時保留兩者,根據需求選擇最適合的工具。


2. 在 Vue3 中使用 Composition API 包裝非同步請求

setup() 函式是 Vue3 組件的入口,我們可以把請求邏輯抽成 可重用的 composable,讓多個組件共享相同的資料取得行為。

// src/composables/useApi.js
import { ref } from 'vue'
import axios from 'axios'

export function useApi(url, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const fetchData = async () => {
    loading.value = true
    error.value = null
    try {
      // 這裡示範以 axios 為主,若想改用 fetch,只要改寫此行即可
      const response = await axios.get(url, options)
      data.value = response.data
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  // 初始化時自動呼叫
  fetchData()

  return { data, loading, error, refetch: fetchData }
}

重點ref 讓資料具備響應式,組件只要解構 data、loading、error 即可自動更新 UI。


3. 使用 fetch 的簡易封裝

// src/composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url, init = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const fetchData = async () => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(url, init)
      if (!response.ok) {
        // 手動拋出錯誤,讓 catch 捕捉
        throw new Error(`HTTP ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  fetchData()

  return { data, loading, error, refetch: fetchData }
}

技巧fetch 只會在網路層面發生錯誤時 reject,必須自行檢查 response.ok 才能捕捉 HTTP 錯誤。


4. 結合全域攔截器:axios 範例

// src/plugins/axios.js
import axios from 'axios'
import { useUserStore } from '@/stores/user' // Pinia store 假設

// 建立 axios 實例
const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 8000,
})

// 請求攔截器:自動帶入 JWT
api.interceptors.request.use(config => {
  const token = useUserStore().token
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 回應攔截器:統一錯誤處理
api.interceptors.response.use(
  response => response,
  err => {
    // 例如:401 自動導向登入頁
    if (err.response?.status === 401) {
      window.location.href = '/login'
    }
    return Promise.reject(err)
  }
)

export default api

說明:將 api 注入到任何 composable 或組件中,就能自動帶入認證資訊,並在全域層面統一處理錯誤。


5. 在組件中同時使用兩種方法

<template>
  <section>
    <h2>使用 axios 取得文章列表</h2>
    <div v-if="axiosLoading">載入中…</div>
    <div v-else-if="axiosError">{{ axiosError.message }}</div>
    <ul v-else>
      <li v-for="item in axiosData" :key="item.id">{{ item.title }}</li>
    </ul>

    <h2>使用 fetch 取得使用者資訊</h2>
    <div v-if="fetchLoading">載入中…</div>
    <div v-else-if="fetchError">{{ fetchError.message }}</div>
    <pre v-else>{{ fetchData }}</pre>
  </section>
</template>

<script setup>
import { useApi } from '@/composables/useApi'
import { useFetch } from '@/composables/useFetch'

// axios 版
const { data: axiosData, loading: axiosLoading, error: axiosError } = useApi('/posts')

// fetch 版
const { data: fetchData, loading: fetchLoading, error: fetchError } = useFetch('/users/1')
</script>

重點:只要把 資料、載入狀態、錯誤訊息ref 形式返回,模板就能簡潔地呈現不同來源的結果。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記處理 fetch 的非 2xx 回應 fetch 不會自動 reject,導致錯誤被忽略。 在取得 response 後檢查 response.ok,必要時自行 throw
請求重複觸發 setup() 中直接呼叫 async 函式,會在每次重新渲染時再次發送。 使用 onMountedwatchEffect 搭配依賴,確保只在需要時觸發。
未取消已掛起的請求 使用者切換頁面前請求仍在執行,可能導致記憶體洩漏或錯誤 UI 更新。 使用 AbortController(fetch)或 axios.CancelToken(axios)在 onUnmounted 時取消。
全域錯誤處理不一致 每個組件自行捕捉錯誤,導致 UI 行為不統一。 透過 axios 攔截器 或自訂 useGlobalError composable 統一處理。
硬編碼 URL API 位址散落各處,環境切換困難。 使用 環境變數 (import.meta.env) 並在 axios.create 中統一設定 baseURL

最佳實踐小結

  1. 統一資料取得方式:在大型專案中,建議以 useApi(axios)為主,僅在簡單 GET 時使用 useFetch
  2. 使用 Pinia / Vuex 管理全域狀態:如認證 Token、全域 Loading。
  3. 加入 TypeScript 型別:即使是 JavaScript,也可以使用 JSDoc 來提升可讀性。
  4. 測試:利用 vitestjest mock axios / fetch,確保非同步邏輯不會因改版而斷裂。

實際應用場景

1. 分頁列表 + 無限捲動

// composables/usePaginatedPosts.js
import { ref, watch } from 'vue'
import api from '@/plugins/axios'

export function usePaginatedPosts() {
  const posts = ref([])
  const page = ref(1)
  const loading = ref(false)
  const error = ref(null)
  const hasMore = ref(true)

  const loadMore = async () => {
    if (loading.value || !hasMore.value) return
    loading.value = true
    try {
      const res = await api.get('/posts', { params: { _page: page.value, _limit: 10 } })
      posts.value.push(...res.data)
      // 判斷是否還有下一頁
      hasMore.value = res.headers['x-total-count'] > posts.value.length
      page.value++
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 初始載入
  loadMore()

  return { posts, loading, error, hasMore, loadMore }
}

在組件中呼叫 loadMore,配合 IntersectionObserver 即可實現無限捲動

2. 表單送出 + 失敗重試

// composables/useSubmitForm.js
import { ref } from 'vue'
import api from '@/plugins/axios'

export function useSubmitForm(endpoint) {
  const submitting = ref(false)
  const error = ref(null)
  const success = ref(false)

  const submit = async (payload, retry = 0) => {
    submitting.value = true
    error.value = null
    success.value = false
    try {
      await api.post(endpoint, payload)
      success.value = true
    } catch (e) {
      if (retry < 2) {
        // 簡易重試機制
        await new Promise(r => setTimeout(r, 1000))
        return submit(payload, retry + 1)
      }
      error.value = e
    } finally {
      submitting.value = false
    }
  }

  return { submitting, error, success, submit }
}

此範例示範 失敗重試(最多兩次),適合 API 可能因短暫網路波動失敗的情況。

3. 使用 AbortController 取消搜尋請求

// composables/useSearch.js
import { ref, watch } from 'vue'

export function useSearch(queryRef) {
  const results = ref([])
  const loading = ref(false)
  const error = ref(null)
  let controller = null

  watch(queryRef, async (newQ) => {
    if (!newQ) {
      results.value = []
      return
    }
    if (controller) controller.abort() // 取消前一次請求
    controller = new AbortController()
    loading.value = true
    error.value = null
    try {
      const res = await fetch(`/search?q=${encodeURIComponent(newQ)}`, {
        signal: controller.signal,
      })
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      results.value = await res.json()
    } catch (e) {
      if (e.name !== 'AbortError') error.value = e
    } finally {
      loading.value = false
    }
  })

  return { results, loading, error }
}

在即時搜尋場景中,取消已發出的請求可以減少伺服器負荷,也避免舊的回應覆寫較新的結果。


總結

  • fetch 為瀏覽器原生、輕量;axios 提供更完整的功能(攔截器、全域設定、錯誤自動 reject)。
  • 在 Vue3 中,利用 Composition API 把非同步邏輯抽成 useApiuseFetchcomposable,可以讓多個組件共享並保持程式碼乾淨。
  • 記得處理 HTTP 錯誤、請求取消、全域錯誤統一,避免常見的陷阱。
  • 透過範例的 分頁、表單重試、即時搜尋,你可以快速將這些技巧帶入實務專案,提升使用者體驗與開發效率。

掌握了上述概念與實作方式,你就能在 Vue3 專案中自如地使用 fetchaxios,建立可維護、可擴充的資料請求層。祝開發順利,持續寫出高品質的前端程式碼!