Vue3 非同步與資料請求:Fetch 與 Axios 整合教學
簡介
在單頁應用 (SPA) 中,資料的取得與送出是最常見的非同步工作。Vue3 內建的 Composition API 讓我們可以更彈性地管理這些非同步流程,但要真正發揮效能,必須了解底層的 HTTP 請求工具——fetch 與 axios。
本篇文章將說明兩者的差異、如何在 Vue3 中優雅地整合,並提供實作範例,讓你在開發過程中能快速、可靠地與後端 API 溝通。
核心概念
1. 為什麼同時保留 fetch 與 axios?
| 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 函式,會在每次重新渲染時再次發送。 |
使用 onMounted 或 watchEffect 搭配依賴,確保只在需要時觸發。 |
| 未取消已掛起的請求 | 使用者切換頁面前請求仍在執行,可能導致記憶體洩漏或錯誤 UI 更新。 | 使用 AbortController(fetch)或 axios.CancelToken(axios)在 onUnmounted 時取消。 |
| 全域錯誤處理不一致 | 每個組件自行捕捉錯誤,導致 UI 行為不統一。 | 透過 axios 攔截器 或自訂 useGlobalError composable 統一處理。 |
| 硬編碼 URL | API 位址散落各處,環境切換困難。 | 使用 環境變數 (import.meta.env) 並在 axios.create 中統一設定 baseURL。 |
最佳實踐小結
- 統一資料取得方式:在大型專案中,建議以
useApi(axios)為主,僅在簡單 GET 時使用useFetch。 - 使用 Pinia / Vuex 管理全域狀態:如認證 Token、全域 Loading。
- 加入 TypeScript 型別:即使是 JavaScript,也可以使用 JSDoc 來提升可讀性。
- 測試:利用
vitest或jestmockaxios/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 把非同步邏輯抽成
useApi、useFetch等 composable,可以讓多個組件共享並保持程式碼乾淨。 - 記得處理 HTTP 錯誤、請求取消、全域錯誤統一,避免常見的陷阱。
- 透過範例的 分頁、表單重試、即時搜尋,你可以快速將這些技巧帶入實務專案,提升使用者體驗與開發效率。
掌握了上述概念與實作方式,你就能在 Vue3 專案中自如地使用 fetch 與 axios,建立可維護、可擴充的資料請求層。祝開發順利,持續寫出高品質的前端程式碼!