為什麼 Nuxt 推薦使用全域的 $fetch
Nuxt 在 HTTP 請求上提供了 $fetch 這個全域的 utils function,這個 utils function 讓我們可以輕鬆取得來自後端的資料,但為什麼 Nuxt 會推薦我們使用 $fetch 而不是其他的 HTTP 請求工具呢?讓我們一起認識 $fetch 並了解它的優勢吧!
前言
在使用 Nuxt 開發 Server Side Rendering 網站時,你會用什麼工具來發送 HTTP 請求呢?
Nuxt 在 HTTP 請求上提供了 $fetch
這個全域的 utils function,這個 utils function 讓我們可以輕鬆取得來自後端的資料,用法如下:
const { data } = useAsyncData(() => {
return $fetch('https://jsonplaceholder.typicode.com/posts')
})
當然,想要使用內部 API 路由取得資料也完全不是問題。
const { data } = useAsyncData(() => $fetch('/api/posts'))
不過有時候我們可能會想要使用我們原本習慣的 HTTP 請求工具,像是 axios 或者 ky-universal 等,難道不可以嗎!?
Nuxt 與 axios 搭配使用無法 Server Side Rendering
我們把上面的範例改寫成使用 axios 的版本:
import axios from 'axios'
const { data } = useAsyncData(() => {
return axios('https://jsonplaceholder.typicode.com/posts').then((res) => res.data)
})
使用起來完全沒有問題,所以想要使用原本習慣的 axios 當然是可以的。
但如果我們想要使用內部 API 路由取得資料,這時候 Server Side Rendering 就會發生錯誤了。
import axios from 'axios'
const { data } = useAsyncData(() => {
return axios('/api/posts').then((res) => res.data).catch((error) => console.error(error))
})
我們來看看錯誤訊息顯示了什麼。
ERROR Invalid URL
at new URL (node:internal/url:775:36)
at dispatchHttpRequest (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/adapters/http.js:232:20)
at node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/adapters/http.js:152:5
at new Promise (<anonymous>)
at wrapAsync (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/adapters/http.js:132:10)
at http (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/adapters/http.js:170:10)
at Axios.dispatchRequest (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/core/dispatchRequest.js:51:10)
at Axios._request (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/core/Axios.js:187:33)
at Axios.request (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/core/Axios.js:40:25)
at wrap (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/helpers/bind.js:5:15)
at Axios.request (node_modules/.pnpm/axios@1.8.4/node_modules/axios/lib/core/Axios.js:45:41)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
而就算改用 Nuxt 使用的 HTTP 工具 ofetch,也一樣會發生錯誤。
import { $fetch } from 'ofetch'
const { data } = useAsyncData(() => {
return $fetch('/api/posts').then((res) => res.data).catch((error) => console.error(error))
})
ERROR [GET] "/api/posts": <no response> Failed to parse URL from /api/posts
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async $fetchRaw2 (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:270:14)
at async $fetch2 (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:316:15)
[cause]: Failed to parse URL from /api/posts
at Object.fetch (node:internal/deps/undici/undici:11372:11)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async $fetchRaw2 (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:258:26)
at async $fetchRaw2 (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:270:14)
at async $fetch2 (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:316:15)
[cause]: Invalid URL
at new URL (node:internal/url:775:36)
at new _Request (node:internal/deps/undici/undici:5055:25)
at fetch2 (node:internal/deps/undici/undici:9195:25)
at Object.fetch (node:internal/deps/undici/undici:11370:18)
at fetch (node:internal/process/pre_execution:282:25)
at node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/node.mjs:26:58
at $fetchRaw2 (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:258:32)
at onError (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:179:16)
at $fetchRaw2 (node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs:270:20)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
我們可以看到 Invalid URL
跟 Failed to parse URL from /api/posts
這兩個錯誤訊息。
上面的做法在 Server Side 會出錯是因為在 Node.js 的環境下,像 /api/posts
這樣的請求並不會自動帶上 host,這樣 Node.js 就無法判斷應該將請求發送到哪裡,因而導致錯誤。
解決方法是發出請求時要確保 axios
或是 ofetch 可以解析出完整的 URL,這樣 Node.js 才能正確解析這個請求。
import axios from 'axios'
const url = useRequestURL()
const { data } = useAsyncData(() => {
return axios(`${url.origin}/api/posts`).then((res) => res.data)
})
import { $fetch } from 'ofetch'
const url = useRequestURL()
const { data } = useAsyncData(() => {
return $fetch(`${url.origin}/api/posts`)
})
但到這裡更令人好奇的是,為什麼直接使用 $fetch
,就不會發生這個問題呢?
Nitro Server 上的 $fetch
在 Nuxt 中我們使用全域的 $fetch
與從 ofetch 導入的 $fetch
是不完全一樣的 function。在 Server Side 我們使用的全域 $fetch
是經由 Nitro Server 重新封裝過的,我們稍微看一下 Nitro Server 是怎麼處理這個部分的。
import { createApp, toNodeListener } from "h3";
import { Headers, createFetch } from "ofetch";
import { fetchNodeRequestHandler, callNodeRequestHandler } from "node-mock-http";
function createNitroApp(): NitroApp {
const h3App = createApp({ ... });
// Create local fetch caller
const nodeHandler = toNodeListener(h3App);
const localFetch: typeof fetch = (input, init) => {
if (!input.toString().startsWith("/")) {
return globalThis.fetch(input, init);
}
// ⬇️ 如果是內部請求走這裡
return fetchNodeRequestHandler(
nodeHandler,
input as string,
init
).then((response) => normalizeFetchResponse(response));
};
const $fetch = createFetch({
fetch: localFetch,
Headers,
defaults: { baseURL: config.app.baseURL },
});
// @ts-ignore
globalThis.$fetch = $fetch;
//...
}
透過上面的實作的片段,我們大概可以略知一二。
ofetch 的 createFetch
允許我們傳入一個自己的 fetch
實作,而 Nitro 傳入的 localFetch
會先判斷要發出的請求是否是內部請求(以 /
開頭的相對路徑),如果是內部請求就會使用 fetchNodeRequestHandler
這個函式與 nodeHandler
來處理後續的邏輯,反之則發使用 Native Fetch API 發出 HTTP 請求。
這也就是為什麼只有 Nuxt 全域的 $fetch
可以正確的處理 /api/posts
請求,而其他 HTTP 函式會出錯的原因。
結語
暸解了 Nitro 對全域 $fetch
的處理後再回頭看 Nuxt 的文件,原本被我忽略的一段話突然變得很有意義:
During server-side rendering, calling
$fetch
to fetch your internal API routes will directly call the relevant function (emulating the request), saving an additional API call.
使用全域 $fetch
,對內部的 API 路由而言就像是呼叫了另外一個 function,它不會發起 HTTP 請求,省去了 HTTP 請求中的 DNS 查找、TCP 連線,也省去了處理 socket、header、解析 JSON 等等開銷。我想這也是為什麼 Nuxt 會建議使用 $fetch
的原因吧!
參考連結
請我喝杯咖啡
如果這裡的內容有幫助到你的話,一杯咖啡就是對我最大的鼓勵。
