# 🏅 Day 9 - $fetch 與 ofetch ## 今日學習目標 - 理解 Nuxt3 基於 ofetch 套件提供了 $fetch 方法 - 學習在 Nuxt3 中使用 $fetch 進行 API 請求 - 了解 $fetch 使用的注意事項 ## 前言 在 Day8 的問題中,我們使用了 Web API Fetch 來取得 API 資料並渲染到畫面上。以下是使用原生 `fetch` 的作法 : ```jsx const roomsList = ref([]); const apiUrl = "https://nuxr3.zeabur.app/api/v1/rooms"; // ↓ Web API 原生的 fetch fetch(apiUrl) .then((response) => { if (!response.ok) { throw new Error("取得房型資料失敗"); } return response.json(); }) .then((data) => { const { result } = data; roomsList.value = result; }) .catch((error) => { console.error("發生錯誤:", error); }); ``` 而在 Nuxt3 整合了 `ofetch` 套件,提供了全域的 `$fetch` 方法,讓我們可以在伺服器和客戶端進行非同步操作。 ## $fetch 的使用方式 Nuxt3 使用 [ofetch](https://github.com/unjs/ofetch) 建立全域的 $fetch 方法,並透過自動匯入 (Auto Imports) 的方式在每個 Vue 元件中可直接使用。因此,我們可以使用 ofetch 定義的屬性與方法格式來發出 API 請求 。 ### $fetch 帶入參數 `$fetch` 方法可以接收多種參數來配置請求。以下針對經常使用的參數進行補充: ```jsx await $fetch(url, { method: "", body: {}, baseURL: "", query: {}, params: {} headers: {}, }) ``` - url : API 請求的 URL。 - method : HTTP 請求的方法,例如 `GET`、`POST`、`PUT`、`PATCH`、`DELETE` 。 - body : 加入 HTTP 請求 body 的物件。ofetch 會自動將物件轉換為 `application/json`格式。 - baseURL : 設定基本的 URL 路徑。在發送請求時,`baseURL` 會自動與 `url` 參數結合。 - query: 查詢參數,以 `?` 的形式附加在 URL 的末尾,例如 `https://www.example.com?key=value`。 - params: 在 ofetch 中 params 是 query 的別名,用法與 query 相同。 - headers: 設定請求的 HTTP 標頭,例如 `Content-Type` 或 `Authorization`。 ### **$fetch 帶入攔截器函式 (interceptors)** 除了帶入參數以外,`$fetch` 還提供了攔截器功能,在請求發出前或收到回應後進行一些處理操作,以下是攔截器的用途: - **onRequest**: : 在發送請求之前執行,用於修改請求的設定,例如添加或修改請求的標頭 ( headers ) 。 - **onRequestError**: 在請求發生錯誤時執行。可以來處理請求錯誤,例如發出錯誤提示訊息。 - **onResponse**: 在接收到回應後執行,可以用來處理回傳的資料。 - **onResponseError**: 當回應發生錯誤時執行,可以用來處理回應錯誤。例如處理伺服器回應的 HTTP 401 狀態碼,並將使用者重新導向至登入頁面。 ```jsx await $fetch("url", { ...其他的參數 onRequest({ request, options }) { // 舉例 : 設置 request headers options.headers = options.headers || {}; options.headers.authorization = `Bearer token XXX`; }, onRequestError({ request, options, error }) { // 處理 request 錯誤 }, onResponse({ request, response, options }) { // 處理回傳資料 return response._data; }, onResponseError({ request, response, options }) { // 處理 response 發生的錯誤 // 舉例:處理伺服器 HTTP 401 status if (response && response.status === 401) { console.error("未授權的訪問,請重新登入"); router.replace('/login'); } }, }); ``` ## $fetch 使用的注意事項 在頁面加載或元件初始化時使用 `$fetch` 進行資料請求(GET)需要注意**重複發送請求**的問題。$fetch 會在伺服器端發送一次請求以進行伺服器端渲染(SSR),然後將請求的回應傳輸到客戶端。當客戶端收到回應後,會再發送一次相同的請求。 ### 重複發送請求的問題 以下範例展示了在 `/page/index.vue` 使用 `$fetch` 請求 [randomuser API](https://randomuser.me/) 的情境: ```html <!-- /page/index.vue --> <script setup> const data = await $fetch("https://randomuser.me/api/"); const [result] = data.results; </script> <template> <img :src="result.picture?.large" :alt="`${result.name.first} ${result.name?.last}`" /> <h1>{{ result.name?.first }} {{ result.name?.last }}</h1> <h3>Email: {{ result.email }}</h3> </template> ``` 執行後會在伺服器端和客戶端都發送一次請求,因此渲染後在瀏覽器 console 會發生 `Hydration text content mismatch` 警告與 `Hydration completed but contains mismatches.` 錯誤 ( 如下圖 ) 。 ![image](https://hackmd.io/_uploads/SkXEiv3Zkx.png) 這個錯誤表示 `{{ data.results }}` 在伺服器端渲染與客戶端渲染的結果不一致。因為在伺服器端,`$fetch` 會發送一次請求來獲取數據並進行 SSR,然後將結果發送到客戶端,而客戶端在接收到 HTML 後,會再次發送一個相同的 `$fetch` 請求。由於 `randomuser` API 每次回應的資料都是隨機的,因此伺服器端與客戶端各自請求一次 API 後,會得到不同的資料,造成渲染不一致的問題。 ### 避免重複發送請求的方法 為了避免伺服器端與客戶端各請求一次 API,同時造成潛在的渲染問題。直接在頁面或是元件取得資料時建議改成使用 [useFetch](https://nuxt.com.cn/docs/api/composables/use-fetch) 或是 [useAsyncData](https://nuxt.com.cn/docs/api/composables/use-async-data) 搭配 **`$fetch`** 來防止重複發送請求。 ```html <!-- /page/index.vue --> <script setup> // 改法一.用 useFetch const { data } = await useFetch("https://randomuser.me/api/"); const [result] = data.value.results; // 改法二.用 useFetch 搭配 $fetch() // const { data } = await useAsyncData("getRandomuser", () => // $fetch("https://randomuser.me/api/") // ); // const [result] = data.value.results; </script> <template> <img :src="result.picture?.large" :alt="`${result.name.first} ${result.name?.last}`" /> <h1>{{ result.name?.first }} {{ result.name?.last }}</h1> <h3>Email: {{ result.email }}</h3> </template> ``` 以上關於 `$fetch` 重複發送請求的特性可以閱讀 [官方文件](https://nuxt.com/docs/api/utils/dollarfetch) 的說明 。而 [useFetch](https://nuxt.com.cn/docs/api/composables/use-fetch) 與 [useAsyncData](https://nuxt.com.cn/docs/api/composables/use-async-data) 會在 Day10 進行介紹 ### 適合使用 $fetch 的情境 如果確定請求是在客戶端由操作者在操作時觸發,例如透過點擊事件送出請求,不是在頁面或是元件初始化時取得資料就可以使用 `$fetch`。例如下方範例,在按鈕點擊事件中使用 `$fetch`就不會發送兩次請求,導致渲染問題 : ```html <!-- /page/index.vue --> <script setup> const userData = ref({}); const getUserData = async () => { const response = await $fetch("https://randomuser.me/api/"); userData.value = response.results[0]; }; </script> <template> <template v-if="userData.name"> <img :src="userData.picture?.large" :alt="`${userData.name?.first} ${userData.name?.last}`" /> <h1>{{ userData.name?.first }} {{ userData.name?.last }}</h1> <h3>Email: {{ userData.email }}</h3> </template> <button type="button" @click="getUserData">取得 User 資料</button> </template> ``` <br> > 今日學習的[範例 Code - 資料夾: day9-nuxt3-fetch-example](https://github.com/hexschool/nuxt-daily-tasks-2024) ## 題目 請 fork 這一份 [模板](https://github.com/jasonlu0525/nuxt3-live-question/tree/day9-nuxt3-fetch),實作帳號註冊功能 : - 在 `/pages/register.vue` 使用模板提供的操作介面填寫註冊表單。點擊 “註冊” 按鈕後使用 Nuxt3 提供的方法串接旅館的 [註冊 API](https://nuxr3.zeabur.app/swagger/#/Users%20-%20%E4%BD%BF%E7%94%A8%E8%80%85/post_api_v1_user_signup) ,將請求送出。 - 需使用 try catch 處理請求成功與失敗的訊息,請求成功與失敗皆使用 [sweetAlert2 套件](https://sweetalert2.github.io/) 顯示訊息。sweetAlert2 套件在模板已有安裝與引入,不需再額外設定。 ```jsx $swal.fire({ position: "center", icon: ... , title: ... , showConfirmButton: false, timer: 1500, }); ``` - 表單不需處理表單驗證、身分驗證、檢查登入狀態以及存入 cookie。 - [註冊 API](https://nuxr3.zeabur.app/swagger/#/Users%20-%20%E4%BD%BF%E7%94%A8%E8%80%85/post_api_v1_user_signup) 夾帶的請求體(Request Body)格式,需要注意以下地方 : - 所有欄位都必填。 - 密碼需要至少 8 碼以上,並英數混合。 - 電話格式可以是手機號碼與市內電話。 - birthday 格式可以是 "yyyy-mm-dd”。 - zipcode 需要對照到各縣市各區的郵遞區號,可以參考 [郵遞區號速查一覽表](https://c2e.ezbox.idv.tw/zipcode.php)。 - 串接 API 時需避免在伺服器端與客戶端重複發送請求。 ![image](https://hackmd.io/_uploads/SyxwovhWyl.png) ## 回報流程 將答案上傳至 GitHub 並複製 GitHub repo 連結貼至底下回報就算完成了喔 ! 解答位置請參考下圖(需打開程式碼的部分觀看) ![](https://i.imgur.com/vftL5i0.png) <!-- 解答: https://github.com/jasonlu0525/nuxt3-live-answer/tree/day9-nuxt3-fetch --> 回報區 --- | # | Discord | Github / 答案 | | --- | ----- | ----- | |1|LinaChen|[GitHub](https://github.com/Lina-SHU/nuxt3-live-question)| |2|眼睛|[GitHub](https://github.com/Thrizzacode/nuxt3-live-question/tree/day9-nuxt3-fetch)| | 3 | Steven | [GitHub](https://github.com/y7516552/nuxt3-live-question/tree/day9) | | 4 | MY | [GitHub](https://github.com/ahmomoz/nuxt3-live-question/tree/day9-nuxt3-fetch-hw) | | 5 | dragon | [GitHub](https://github.com/peterlife0617/2024-nuxt-training-homework01/tree/feature/day09) | | 6 | Tough life | [GitHub](https://github.com/hakuei0115/Nuxt_testing) | | 7 | NeiL | [GitHub](https://github.com/Neil10241126/nuxt-demo/tree/day9-nuxt3-fetch) | |8|kevinhes|[Github](https://github.com/kevinhes/nuxt-daily-mission/tree/day10)| |9|Jim Lin|[Github](https://github.com/junhoulin/Nuxt3-hw-day9)| | 10 | hsin yu | [GitHub](https://github.com/dogwantfly/nuxt3-daily-task-live-question/tree/day9-nuxt3-fetch) | | 11 | Stan | [GitHub](https://github.com/haoxiang16/nuxt3-live-question/tree/day9-nuxt3-fetch) | | 12 | Rocky | [GitHub](https://github.com/WuRocky/Nuxt-Day9--fetch-ofetch.git) | | 13 |Fabio20 | [GitHub](https://github.com/fabio7621/nuxt3-live-question-d2/tree/nuxt3-live-day9) | | 14 | Ariel| [GitHub](https://github.com/Ariel0508/nuxtday9) | | 15| wei_Rio| [GitHub](https://github.com/wei-1539/nuxtDaily9)| | 16 | runweiting | [GitHub](https://github.com/runweiting/2024-NUXT-Task/tree/day9) | | 17 | hannahTW | [GitHub]( https://github.com/hangineer/nuxt3-live-question/tree/day9-nuxt3-fetch) | | 18 | Nielsen | [GitHub](https://github.com/asz8621/nuxt3-daily-task/tree/day9-nuxt3-fetch) | | 19 | 阿塔 | [GitHub](https://github.com/QuantumParrot/2024-Hexschool-Nuxt-Camp-Daily-Task) | | 20 | tanuki狸 | [GitHub](https://github.com/tanukili/Nuxt-2024-week01-2/tree/day9-nuxt3-fetch) | | 21 | barry1104 | [GitHub](https://github.com/barrychen1104/nuxt3-live-question/tree/day9-nuxt3-fetch) | |22|好了啦|[GitHub](https://github.com/ZhangZJ0906/nuxt-live)| |23|Johnson|[GitHub](https://github.com/tttom3669/2024_hex_nuxt_daily/tree/day9-nuxt3-fetch)| | 24 | lidelihn | [GitHub](https://github.com/Lide/nuxt3-live-question/tree/day9-nuxt3-fetch) | <!-- 快速複製 | --- | ----- | [GitHub]() | -->