藥不藥來當工程師一個不務正業的藥師,誤打誤撞來當了前端工程師。斜槓工程師的人生!

Nuxt3 中 $fetch 與 axios 在模組頂層的差異

發佈於 最後更新於
7 min read

最近在重構公司專案的 API 層時,遇到了一個有趣的問題:原本用 axios 可以正常運作的程式碼,改用 Nuxt3 的 $fetch 後卻報錯了。這個問題讓我深入研究了 $fetch 和 axios 在底層實作上的差異。

問題描述

我的 API 層原本是這樣寫的:

typescript
// 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 時:

typescript
// 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

typescript
// 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 的話,同樣的寫法卻完全沒問題:

typescript
// 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)
  }
})
typescript
// 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 $fetch helper for making HTTP requests within your Vue app or API routes.

重點在於「globally」這個字。$fetch 本身確實是全域可用的,但透過 nuxtApp.provide() 提供的 $request 卻不是。

useNuxtApp() 需要在 Nuxt Runtime Context 內執行3

當我們在模組頂層(module scope)執行 useNuxtApp() 時:

typescript
const { $request } = useNuxtApp()  // 在模組頂層執行

這時候 Nuxt app 還沒有完全初始化,所以 $request 會是 undefined

但為什麼 useRuntimeConfig() 可以在模組頂層執行?因為 Nuxt 在編譯時期就已經處理好 runtime config,它不需要等待 app 初始化。

axios 是普通的 JavaScript 物件

axios 不同,它是一個獨立的 HTTP client library:

typescript
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(不推薦)

typescript
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(推薦)

typescript
// 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
      }
    },
  })
}
typescript
// 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 的優勢主要在於:

  1. SSR 優化:在 server-side 呼叫內部 API 時,會直接呼叫對應的函式,不會真的發 HTTP request
  2. 更輕量:不需要額外安裝 axios
  3. Nuxt 原生整合:自動處理 base URL、錯誤處理等

但對於 SSG(Static Site Generation)專案來說,這些優勢就沒那麼明顯了。

總結

這次的問題讓我學到:

  1. $fetch 是 composable,需要在正確的 context 執行
  2. axios 是普通物件,可以在任何地方使用
  3. 不要為了用新技術而重構,要看實際需求

如果你的專案是 SSG,而且已經在用 axios,那就繼續用吧。如果是 SSR 專案,而且想要優化效能,那 $fetch 會是更好的選擇,但要注意使用方式。

最後,記得:工具沒有絕對的好壞,只有適不適合你的場景

Reference

  1. ofetch - GitHub
  2. $fetch - Nuxt
  3. useNuxtApp - Nuxt
Copyright 2020-2025 - AzureBlue ALL RIGHTS RESERVED.