在 Nuxt 3 中,你可以直接使用 useAsyncData
、useFetch
、useLazyAsyncData
、useLazyFetch
函式來發送網路或 API 請求,這幾種方式其實都只是 Nuxt 封裝 unjs/ofetch 的組合式函式,如果你想直接使用,Nuxt 也提供了全域可以使用 $fetch
helper。
以前在寫 JavaScript 實作 Ajax 技術時,都是直接使用 XMLHttpRequest 從頭刻,直到有 jQuery 的 $.Ajax() 等這類封裝的 HTTP 請求的函式庫或 HTTP Client,發送 API 請求才相對容易一些。
隨著 ESMAScript 的標準迭代推進,有了新的 Fetch API,可以來操作 HTTP 請求與回傳,使得非同步的資料獲取也變得簡單易懂。
雖然我們有了 Fetch API 可以很方便的發送 HTTP 請求,但仍有一些不足的地方,相信多數前端工程師一定都聽過 Axios,它是一個基於 Promise 的 HTTP Client,Axios 不僅在瀏覽器,連後端的 Node.js 也能夠使用,雖然需要額外安裝,但是解決了原生 fetch 對於請求 Payload 或回傳類型的轉換,例如 JSON 可以很直覺的直接使用,甚至在異常或需要中斷請求的操作等等功能都更加方便。
你可能會想我以前已經很習慣使用 Axios 了,我在 Nuxt 可以繼續使用嗎?其實是可以的,但是不推薦。因為 Nuxt 所封裝的 $fetch
helper,會相較於 Axios 有更多的優點與實用的功能函式。
Nuxt 提供了全域可以使用的 $fetch
helper,$fetch
是基於 unjs/ofetch 的封裝,除了類似原生 fetch 更擁有 Axios 等等優點,讓你可以使用 ofetch 來替代 Axios,而且還有 Nuxt 封裝的組合式函式 useFetch
與其他特點。
在伺服器渲染期間,如果有使用 $fetch
來呼叫伺服器本身提供的內部 Server API,Nuxt 將會直接模擬請求互叫內部 Server API 所實作的函式,從而減少額外的 API 請求呼叫。
如果在不熟悉 Nuxt 預設的通用渲染模式 (Universal Rendering) 下作開發,那麼在使用 $fetch
可能會發生一些問題,比較常見的是你發現到程式好像發送了兩次 API 或外部網路請求。
舉個例子,你在 ./pages/post.vue 檔案撰寫了如下取得貼文的相關程式:
<script setup>
const posts = await $fetch('/api/posts')
</script>
上述簡單的一段程式來獲取貼文的資料,但是在實際運作流程可能會發生一些問題,以預設的通用渲染為例:
這個過程,簡單來說就是在伺服器端獲取了一次貼文資料,客戶端又再一次的請求相同資料,導致 API 重複的呼叫,如此一來不僅對伺服器 API 造成負擔,也可能因為 API 有相關副作用,如新增或刪除等操作而不可預期。
Nuxt 3 提供了一個組合式函式 useAsyncData
,在頁面、元件或插件中可以使用,這個函式最重要的特性就是,它返回的響應式狀態會添加在 Nuxt 的 Payload 中一起在首次渲染 HTML 時回傳給客戶端,所以當伺服器呼叫使用了這個函式來設定貼文資料,那麼在客戶端的 Hydration 階段,因為資料已經存在在 Nuxt 的 Payload 中,所以無需在發送 API 進行請求。
useAsyncData
通常與非同步需要請求資料的邏輯一起使用,使用 useAsyncData
可以傳入一個唯一的 key,在伺服器端負責請求並寫入 Nuxt 的 Payload ,而客戶端依據這個 key 檢查 Nuxt 的 Payload 中是否有相同的資料,就可以直接取出做使用。
這邊也有個小誤解提醒,useAsyncData
並不是直接傳入網址來發送 API 請求,而是與 $fetch
進行搭配來發送 API 請求,並解決上述 Hydration 階段重複發送請求得問題。
所以當你看官網的範例,應該會是 useAsyncData
與 $fetch
搭配一起使用,第一個參數就是唯一的 key,第二個參數就是取得資料的函式,這邊是使用 $fetch
:
<script setup>
const { data } = await useAsyncData('posts', () => $fetch('/api/posts'))
</script>
使用 useAsyncData
時,如果沒有特別需求或比較常是不傳入唯一的 key,交由 Nuxt 來依據檔案名稱與程式碼的行號來產生唯一的 key,以此確保伺服器端與客戶端在同一行程式碼的 API 請求,在 Hydration 階段不會重複發送。
<script setup>
const { data } = await useAsyncData(() => $fetch('/api/posts'))
</script>
根據上述幾個例子,useAsyncData
與 $fetch
會是一個在 Nuxt 蠻常使用的一個組合,所以 Nuxt 3 提供了一個方便的組合式函式 useFetch
,它是 useAsyncData
搭配 $fetch
的封裝,useFetch
可以根據請求 API 的 URL 與選項,自動產生 useAsyncData
需要的唯一 key,甚至能在使用伺服器內部 API 時,提供類型提示與推斷回應的類型。
在撰寫 API 請求就會非常的容易與優雅。
<script setup>
const { data } = await useFetch('/api/posts')
</script>
useLazyAsyncData
僅是為 useAsyncData
組合式函式的選項 lazy
預設為 ture 的一個封裝,useLazyFetch
與 useFetch
關係亦同。
雖然有了 useFetch
就可以解決大部分發送 API 請求的情境,而 $fetch
也並非一無是處,你只要確保你了解渲然模式與在伺服器與客戶端發送請求時機,那麼仍然可以在專案內使用它。
舉例來說,想要刪除一篇貼文,因為這個事件是在客戶端使用者進行操作交互時才會觸發,也可能不需要獲得響應式的回傳狀態或重新整理,所以我們可以很放心的直接使用 $fetch
來發送刪除的請求,也可以處理這個非同步請求 then 與 catch 來提示刪除請求成功或失敗。
const handleDeletePostById = (postId) => {
$fetch(`/api/posts/${postId}`, {
method: 'DELETE',
})
}
pending
狀態、控制執行與刷新 refresh
/ execute
等,也是專案中常使用的組合式函式。process.client
或 process.server
屬性來判斷僅在客戶端或伺服器端來執行 $fetch,以避免 Hydration 階段重複發送請求。看完了本篇 $fetch 與 useFetch 的使用時機,你可能會說 $fetch 使用時多以客戶端的交互事件所觸發,那如果頁面上請求資料的分頁處理,可能是使用者點擊頁數來做切換,不就是要使用 $fetch 嗎?
當然並不完全是這樣的,useFetch 提供了 refresh 與 watch params 的方法其實可以更方便你做資料的分頁,那我們就不一定需要只限使用 $fetch,有更好的組合式函式難道不用嗎。所以 $fetch 與 useFetch 只要你暸解他的功能性,時機上的選擇只是大多數的參考,實務層面還是可以綜合考量後來做決定。
感謝大家的閱讀,歡迎大家給予建議與討論,也請各位大大鞭小力一些:)
如果對這個 Nuxt 3 系列感興趣,可以訂閱
接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。
參考資料
想請問 Ryen 大,
在 axios 中有提供 create 方式建立出 instance 方便統一管理,在 nuxt3 中有同樣或是推薦的方式來統一管理 API 嗎?
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
如果使用的是 $fetch,一樣可以透過 create
來建立 instance
const apiFetch = $fetch.create({
baseURL: 'https://some-domain.com/api',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' }
})
或者你也可以使用組合式函式來二次封裝 useFetch
// composables/useApiFetch.js
const useApiFetch = (url, options) => {
return useFetch(url, {
baseURL: 'https://some-domain.com/api',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' },
...options
})
}
export default useApiFetch
<script setup>
// [GET] https://some-domain.com/api/test
const { data } = await useApiFetch('/test')
</script>
謝謝解答!
同樣有這個疑問!太感謝了