iT邦幫忙

2023 iThome 鐵人賽

DAY 9
1
Vue.js

Nuxt 3 實戰筆記系列 第 9

[Day 09] Nuxt 3 發送 API 請求資料 - 從 $fetch 與 useAsyncData 到 useFetch

  • 分享至 

  • xImage
  •  

前言

在 Nuxt 3 中,你可以直接使用 useAsyncDatauseFetchuseLazyAsyncDatauseLazyFetch 函式來發送網路或 API 請求,這幾種方式其實都只是 Nuxt 封裝 unjs/ofetch 的組合式函式,如果你想直接使用,Nuxt 也提供了全域可以使用 $fetch helper。

Fetch 與 Axios

以前在寫 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 3 的 $fetch

Nuxt 提供了全域可以使用的 $fetch helper,$fetch 是基於 unjs/ofetch 的封裝,除了類似原生 fetch 更擁有 Axios 等等優點,讓你可以使用 ofetch 來替代 Axios,而且還有 Nuxt 封裝的組合式函式 useFetch 與其他特點。

在伺服器渲染期間,如果有使用 $fetch 來呼叫伺服器本身提供的內部 Server API,Nuxt 將會直接模擬請求互叫內部 Server API 所實作的函式,從而減少額外的 API 請求呼叫

$fetch 重複發送請求

如果在不熟悉 Nuxt 預設的通用渲染模式 (Universal Rendering) 下作開發,那麼在使用 $fetch 可能會發生一些問題,比較常見的是你發現到程式好像發送了兩次 API 或外部網路請求。

舉個例子,你在 ./pages/post.vue 檔案撰寫了如下取得貼文的相關程式:

<script setup>
const posts = await $fetch('/api/posts')
</script>

上述簡單的一段程式來獲取貼文的資料,但是在實際運作流程可能會發生一些問題,以預設的通用渲染為例:

  1. 首先使用者從瀏覽器發送了瀏覽 /post 頁面的請求,伺服器端開始進行渲染,取得貼文資料後回傳渲染出來的 HTML 給客戶端。
  2. 客戶端庫收到頁面請求回應,依據回傳的 HTML 網頁原始碼,瀏覽器開始繪製畫面至完成。
  3. 繪製期間後為了後續與交互,開始請求頁面相關的 Vue 等 JavaScript 程式碼,這個 Hydration 過程,會在執行一遍相同的 /post 頁面程式碼,也就造成了 $fetch 在客戶端又發送了一次請求給 Server API。
  4. 最後 Hydration 階段完成,網頁開始有交互性,並轉由客戶端渲染 (CSR)。

這個過程,簡單來說就是在伺服器端獲取了一次貼文資料,客戶端又再一次的請求相同資料,導致 API 重複的呼叫,如此一來不僅對伺服器 API 造成負擔,也可能因為 API 有相關副作用,如新增或刪除等操作而不可預期。

useAsyncData

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>

useFetch

根據上述幾個例子,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 與 useLazyFetch

useLazyAsyncData 僅是為 useAsyncData 組合式函式的選項 lazy 預設為 ture 的一個封裝,useLazyFetchuseFetch 關係亦同。

$fetch 與 useFetch 使用時機

雖然有了 useFetch 就可以解決大部分發送 API 請求的情境,而 $fetch 也並非一無是處,你只要確保你了解渲然模式與在伺服器與客戶端發送請求時機,那麼仍然可以在專案內使用它。

舉例來說,想要刪除一篇貼文,因為這個事件是在客戶端使用者進行操作交互時才會觸發,也可能不需要獲得響應式的回傳狀態或重新整理,所以我們可以很放心的直接使用 $fetch 來發送刪除的請求,也可以處理這個非同步請求 then 與 catch 來提示刪除請求成功或失敗。

const handleDeletePostById = (postId) => {
  $fetch(`/api/posts/${postId}`, {
    method: 'DELETE',
  })
}
  • useFetch: 只要注意它的參數用法與回傳的資料具有響應性,進行 API 請求幾乎都可以使用它,因為回傳的響應式狀態非常豐富,可以得知請求進行中的 pending 狀態、控制執行與刷新 refresh / execute 等,也是專案中常使用的組合式函式。
  • $fetch: 使用時多以客戶端的交互事件所觸發,例如點擊送出表單的請求,或是以 process.clientprocess.server 屬性來判斷僅在客戶端或伺服器端來執行 $fetch,以避免 Hydration 階段重複發送請求。

小結

看完了本篇 $fetch 與 useFetch 的使用時機,你可能會說 $fetch 使用時多以客戶端的交互事件所觸發,那如果頁面上請求資料的分頁處理,可能是使用者點擊頁數來做切換,不就是要使用 $fetch 嗎?
當然並不完全是這樣的,useFetch 提供了 refresh 與 watch params 的方法其實可以更方便你做資料的分頁,那我們就不一定需要只限使用 $fetch,有更好的組合式函式難道不用嗎。所以 $fetch 與 useFetch 只要你暸解他的功能性,時機上的選擇只是大多數的參考,實務層面還是可以綜合考量後來做決定。


感謝大家的閱讀,歡迎大家給予建議與討論,也請各位大大鞭小力一些:)
如果對這個 Nuxt 3 系列感興趣,可以訂閱接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。

參考資料


上一篇
[Day 08] Nuxt 3 如何得知路由名稱與自定義頁面路由
下一篇
[Day 10] Nuxt 3 控制伺服器端及客戶端的渲染執行與 ClientOnly 的使用
系列文
Nuxt 3 實戰筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
佐料
iT邦新手 3 級 ‧ 2023-09-29 17:56:07

想請問 Ryen 大,

在 axios 中有提供 create 方式建立出 instance 方便統一管理,在 nuxt3 中有同樣或是推薦的方式來統一管理 API 嗎?

const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});
Ryan iT邦新手 1 級 ‧ 2023-09-29 18:58:10 檢舉

如果使用的是 $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>
佐料 iT邦新手 3 級 ‧ 2023-09-30 13:48:48 檢舉

謝謝解答!

同樣有這個疑問!太感謝了

我要留言

立即登入留言