Nuxt3 - Data Fetching

簡介

  • Nuxt3 提供了基於 ofetch 封裝的 composables 方法來做 data fetching
  • 根據需求,可以在 server 端或 client 端使用

與 Server Side Rendering 的關係

Nuxt 的 SSR 簡單步驟如下

  1. 首次載入頁面,從 server 端發出 API 請求 (Data Fetching)
  2. 把回傳的資料嵌入 HTML (原始碼有利於爬蟲,加強 SEO)
  3. 回傳完整的 HTML 到客戶端
  4. 使用者看到畫面

useAsyncData

  • 在伺服器端發 API 請求使用
  • 建議直接在<script setup> </script> 內當成一個生命週期使用
  • 在伺服器端,它將在渲染之前被調用
  • 在客戶端,它將在組件首次渲染之前被調用
  • 需要有一個唯一的 key,避免在 client 端重覆傳送請求
  • 沒有設定 key 的話會自動生成一個對應檔案名和編號的唯一 key
  • 使用場景:當頁面一載入就需要請求 API 資料
<script setup>const { data, pending, error, refresh } = await useAsyncData('count', () => $fetch('/api/count'))</script><template>  {{ data }}</template>

🍔 The first argument of useAsyncData is the unique key used to cache the response of the second argument, the querying function. This argument can be ignored by directly passing the querying function. In that case, it will be auto-generated.

可以傳入的參數

  • key
    • 唯一鍵,可以確保資料不會重複的獲取,也就是如果 Key 相同便不會再發送相同的請求,除非重新整理頁面由後端再次渲染獲取,或呼叫  useAsyncData  回傳的  refresh()  函數重新取得資料
  • handler
    • 回傳異步請求資料的處理函數,打 API 或加工的異步邏輯都可以在這裡處理
  • options:
    • server: 是否在伺服器端獲取資料,預設為  true
    • lazy: 是否於載入路由後才開始執行異步請求函數,預設為  false,所以會阻止路由載入直到請求完成後才開始渲染頁面元件
    • default: 當傳入這個 factory function,可以將異步請求發送與回傳解析前,設定資料的預設值,對於設定  lazy: true  選項特別有用處,至少有個預設值可以使用及渲染顯示
    • transform: 修改加工  handler  回傳結果的函數
    • pickhandler  若回傳一個物件,只從中依照需要的 key 取出資料,例如只從 JSON 物件中取的某幾個 key 組成新的物件
    • watch: 監聽  ref  或  reactive  響應式資料發生變化時,觸發重新請求資料,適用於資料分頁、過濾結果或搜尋等情境
    • initialCache: 預設為  true,當第一次請求資料時,將會把有效的 payload 快取,之後的請求只要是相同的 key,都會直接回傳快取的結果
    • immediate: 預設為  true,請求將會立即觸發

收到的回傳值

  • data: the result of the asynchronous function that is passed in
  • pending: a boolean indicating whether the data is still being fetched
  • refresh/execute: a function that can be used to refresh the data returned by the handler function
  • error: an error object if the data fetching failed

$fetch

  • 如果要封裝建議使用$fetch
  • 如果直接在<script setup></script>中使用,會先在 server 端請求資料,又在 client 端重覆請求
  • 使用場景:
    • 在客戶端使用者發送請求 (ex: 送出表單)
    • 在伺服器端發送 API 請求時使用
const users = await $fetch('/api/users').catch((error) => error.data)

在 Server Side 發送 API 請求

  • server/api/pokemon.js
// Server Sideexport default defineEventHandler(async (event) => {  const data = await $fetch(`https://pokeapi.co/api/v2/pokemon/charizard`)  return data})
  • 從 client 端傳入動態路徑
  • server/api/[pokemon].js
export default defineEventHandler(async (event) => {  const { pokemon } = event.context.params  const data = await $fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)  return data})

封裝範例

const apiFetch = $fetch.create({  baseURL: 'https://some-domain.com/api',  timeout: 1000,  headers: { 'Content-Type': 'application/json' }})

useFetch

  • useAsyncData 和$fetch 的組合方法
  • 建議不要當成 axios 來封裝使用
  • 建議不要包裝在函式、生命週期(onMounted….)、watch….等
// 相當於useAsyncData(url, () => $fetch(url))//例如const { data } = await useFetch(url)//等同於const { data } = await useAsyncData('nuxt3Test', () => {  return $fetch(url)})
<script setup>const { data: count } = await useFetch('/api/count')</script><template>Page visits: {{ count }}</template>
  • Get 請求:帶入 query 參數寫法
const { data } = await useFetch(url, {  query: { page: 1, list: 20 }})
  • Options (extends unjs/ofetch options & AsyncDataOptions):
    • method: HTTP 請求方法
    • query: 將參數透過?的方式帶到 URL 上
    • params: 將參數帶到 URL 上
    • body: Request body - automatically stringified (if an object is passed).
    • headers: Request headers.
    • baseURL: 基本的 API URL 路徑

加上 Lazy

  • useLazyFetch, useLazyAsyncData
  • 會先載入頁面,顯示靜態內容,等取得資料後再渲染畫面
  • 和直接加入參數lazy: true的效果相同
  • 需要處理一開始 data 為null的狀態(ex: 加上 Loading 提示)
const { pending, data: posts } = useLazyFetch('/api/posts')// 等同於const { pending, data: posts } = useLazyFetch('/api/posts', {  lazy: true})
  • 一開始 data 為null,可使用watchEffect監聽,當 data 有值時,再做動作
const latestArticles = ref<ArticleCard[]>([])const { data } = await useLazyFetch<ApiResponse>('/api/allarticles', {  method: 'POST',  body: {    Page: 1,    UserId: 'abc'  }})watchEffect(() => {  if (data.value) {    latestArticles.value = data.value?.LatestArticleData  }})

重新請求資料

手動更新

  1. 透過refresh方法進行刷新
<script setup lang="ts">const { data, error, refresh } = await useFetch('/api/users')</script><template>  <div>    <p>{{ data }}</p>    <button @click="refresh">Refresh data</button>  </div></template>
  1. 透過唯一的 key 使用refreshNuxtData重新觸發抓取資料
const { data } = await useAsyncData('userInfo', () => {  return $fetch('/api/auth/userInfo')})const refreshGetData = () => {  refreshNuxtData('userInfo')}

自動更新

  • 寫法 1:當參數是響應式時(ref, reactive, computed),若參數的值改變,useFetch 會自動重新發送請求
  • 範例 1:當 id 改變時,資料也會同步更新
<script setup>const id = ref(1)const { data } = await useFetch(`https://jsonplaceholder.typicode.com/todos`, {  params: { id }})</script><template>  <div>    {{ data }}    <button type="button" class="btn-sm" @click="id--">id - 1</button>    <button type="button" class="btn-sm" @click="id++">id + 1</button>  </div></template>
  • 範例 2:當 nowPage 改變時,useFetch 會被觸發,取得更新的資料
<script setup>const nowPage = ref(1)const lastPage = () => {  nowPage.value--}const nextPage = () => {  nowPage.value++}const { data } = await useFetch(`${apiBase}/readallarticles`, {  method: 'POST',  body: {    // 如果傳入的是nowPage.value,不會觸發自動更新    Page: nowPage,    UserId: userData.value.id || '0'  }})</script><template>  {{ data }}  <button type="button" @click="lastPage">上一頁</button>  <button type="button" @click="nextPage">下一頁</button></template>
  • 寫法 2:加入 watch 參數監聽
  • 範例
<script setup>const page = ref(1)const { data: posts } = await useAsyncData(  'posts',  () =>    $fetch('https://fakeApi.com/posts', {      params: {        page: page.value      }    }),  {    watch: [page]  })</script>

攔截器 interceptors

const { data, pending, error, refresh } = await useFetch('/api/auth/login', {  onRequest({ request, options }) {    // 設置 request headers    options.headers = options.headers || {}    options.headers.authorization = 'Bearer token'  },  onRequestError({ request, options, error }) {    // 處理 request 錯誤  },  onResponse({ request, response, options }) {    // 處理回傳資料    // 要記得return 才能用data取得資料    return response._data  },  onResponseError({ request, response, options }) {    // 處理回傳錯誤  }})

多個 API 請求

  • 如果一個一個執行,會造成阻塞
const { data: userData } = await useFetch(url1)const { data: areaData } = await useFetch(url2)
  • 可以使用 Promise.all 一起執行,都有資料後再回傳
const [{ data: userData }, { data: areaData }] = await Promise.all([useFetch(url1), useFetch(url2)])

避免重複發 API 請求

  • 可以在 pinia 內建立一個isFetch狀態限制
  • 只要isFetch 為 true,代表已發送請求正在等待回應,此時無法再次請求
// stores / vote.jsexport const useVoteStore = defineStore('vote', () => {  const voteData = ref({})  const isFetch = ref(false)  const setVoteData = (data) => {    voteData.value = data  }  const addVote = async (type) => {    if (isFetch.value) return    isFetch.value = true    try {      const data = await $fetch(url, {        method: 'POST',        body: { type }      })      voteData.value = data    } catch (error) {      alert('錯誤')    } finally {      isFetch.value = false    }  }  return { voteData, isFetch, setVoteData, addVote }})

心得

目前在使用 Nuxt3 的 fetch 方法感覺還不是很熟練,要先確認需求要在 Server 還是 Client 端請求,有時候還會遇到Hydration mismatch的問題,也不像axios可以封裝管理,在專題時寫了不少重複的 code,還需要多練習和多看一些範例學習。

參考資料

# Nuxt3