Nuxt3 中 $fetch 與 axios 在模組頂層的差異
最近在重構公司專案的 API 層時,遇到了一個有趣的問題:原本用 axios 可以正常運作的程式碼,改用 Nuxt3 的 $fetch 後卻報錯了。這個問題讓我深入研究了 $fetch 和 axios 在底層實作上的差異。
問題描述
我的 API 層原本是這樣寫的:
// api/products/api.ts
const { public: { API_BASE_URL } } = useRuntimeConfig()
export async function fetchProductList() {
const resp = await $fetch('/api/products', {
baseURL: API_BASE_URL,
})
return resp
}
這樣寫用 $fetch 完全沒問題。但當我想要透過 plugin 提供一個自訂的 $request 來自動帶上 Auth token 時:
// plugins/request.ts
export default defineNuxtPlugin({
name: 'request',
setup(nuxtApp) {
const { $auth } = useNuxtApp()
const request = $fetch.create({
async onRequest({ options }) {
const token = await $auth.getAccessToken()
if (token) {
options.headers.set('Authorization', `Bearer ${token}`)
}
}
})
nuxtApp.provide('request', request)
}
})
然後在 API 層改用 $request:
// api/products/api.ts
const { public: { API_BASE_URL } } = useRuntimeConfig()
const { $request } = useNuxtApp() // ❌ 這裡會出錯!
export async function fetchProductList() {
const resp = await $request('/api/products', {
baseURL: API_BASE_URL,
})
return resp
}
結果瀏覽器 console 就噴錯了:
TypeError: $request is not a function
奇怪的是,如果我用 axios 的話,同樣的寫法卻完全沒問題:
// plugins/request.ts (axios 版本)
import axios from 'axios'
export default defineNuxtPlugin({
name: 'request',
setup(nuxtApp) {
const { $auth } = useNuxtApp()
const request = axios.create()
request.interceptors.request.use(async (config) => {
const token = await $auth.getAccessToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
nuxtApp.provide('request', request)
}
})
// api/products/api.ts (axios 版本)
const { public: { API_BASE_URL } } = useRuntimeConfig()
const { $request } = useNuxtApp() // ✅ axios 版本可以正常運作
export async function fetchProductList() {
const resp = await $request.get('/api/products', {
baseURL: API_BASE_URL,
})
return resp.data
}
這到底是為什麼?
底層原因分析
$fetch 是 Nuxt 的 Auto-import Composable
$fetch 是 Nuxt 基於 ofetch1 提供的全域 auto-import。根據 Nuxt 官方文件2:
Nuxt uses ofetch to expose globally the
$fetchhelper for making HTTP requests within your Vue app or API routes.
重點在於「globally」這個字。$fetch 本身確實是全域可用的,但透過 nuxtApp.provide() 提供的 $request 卻不是。
useNuxtApp() 需要在 Nuxt Runtime Context 內執行3
當我們在模組頂層(module scope)執行 useNuxtApp() 時:
const { $request } = useNuxtApp() // 在模組頂層執行
這時候 Nuxt app 還沒有完全初始化,所以 $request 會是 undefined。
但為什麼 useRuntimeConfig() 可以在模組頂層執行?因為 Nuxt 在編譯時期就已經處理好 runtime config,它不需要等待 app 初始化。
axios 是普通的 JavaScript 物件
axios 不同,它是一個獨立的 HTTP client library:
import axios from 'axios'
const request = axios.create() // 這是一個普通的 JavaScript 物件
當你透過 nuxtApp.provide('request', request) 提供它時,它只是把這個物件掛到 Nuxt app 上。在模組頂層執行 useNuxtApp() 時,雖然時機不對,但因為 axios instance 本身就是一個普通物件,所以還是可以正常運作。
簡單來說:
- $fetch.create() 回傳的是一個需要 Nuxt context 的函式
- axios.create() 回傳的是一個普通的 JavaScript 物件
解決方案
既然知道問題出在「模組頂層無法正確取得 Nuxt context」,那解決方案就很明確了:
方案 1:在函式內部取得 $request(不推薦)
export async function fetchProductList() {
const { $request } = useNuxtApp() // 在函式內部執行
const { public: { API_BASE_URL } } = useRuntimeConfig()
const resp = await $request('/api/products', {
baseURL: API_BASE_URL,
})
return resp
}
這樣可以運作,但每個函式都要重複這段程式碼,很醜。
方案 2:建立 Utility Function(推薦)
// api/request.ts
export function createAuthenticatedFetch() {
const { $auth } = useNuxtApp()
return $fetch.create({
async onRequest({ options }) {
const token = await $auth.getAccessToken()
if (token) {
const headers = new Headers(options.headers)
headers.set('Authorization', `Bearer ${token}`)
options.headers = headers
}
},
})
}
// api/products/api.ts
import { createAuthenticatedFetch } from '~/api/request'
const { public: { API_BASE_URL } } = useRuntimeConfig()
export async function fetchProductList() {
const request = createAuthenticatedFetch() // 在函式內部建立
const resp = await request('/api/products', {
baseURL: API_BASE_URL,
})
return resp
}
這個方案的優點:
- ✅ 明確看出哪些 API 需要認證
- ✅ 不需要 plugin
- ✅ 靈活性高(有些 API 可能不需要認證)
- ✅ 符合 Nuxt 的設計理念
方案 3:繼續用 axios
如果你的專案已經在用 axios,而且運作良好,其實沒必要為了用 $fetch 而重構。axios 的 interceptor 機制很成熟,而且在這個場景下沒有明顯的劣勢。
$fetch 的優勢在哪?
你可能會問:既然 $fetch 有這些限制,為什麼還要用它?
$fetch 的優勢主要在於:
- SSR 優化:在 server-side 呼叫內部 API 時,會直接呼叫對應的函式,不會真的發 HTTP request
- 更輕量:不需要額外安裝 axios
- Nuxt 原生整合:自動處理 base URL、錯誤處理等
但對於 SSG(Static Site Generation)專案來說,這些優勢就沒那麼明顯了。
總結
這次的問題讓我學到:
- $fetch 是 composable,需要在正確的 context 執行
- axios 是普通物件,可以在任何地方使用
- 不要為了用新技術而重構,要看實際需求
如果你的專案是 SSG,而且已經在用 axios,那就繼續用吧。如果是 SSR 專案,而且想要優化效能,那 $fetch 會是更好的選擇,但要注意使用方式。
最後,記得:工具沒有絕對的好壞,只有適不適合你的場景。