# 🏅 Day 14 - 登入功能與存入 Cookie ## 今日學習目標 - 學習如何在 Nuxt3 中實作登入功能,並透過 Cookie 存取與維護使用者的登入狀態 - 學習使用 Nuxt3 useCookie Composable 存入 Token ## 前言 在 Day9 的問題中,我們使用 Nuxt3 `$fetch` 實作了註冊帳號的功能,從客戶端發送註冊請求。今天我們將探討如何在註冊帳號之後,使用 Nuxt3 實作登入功能並將 Token 存入 Cookie 以維持使用者的登入狀態。 ### 範例準備 今天將使用以下兩支 API 進行講解。請求帶入資料與成功、失敗的訊息格式可以閱讀 [API 文件](https://todolist-api.hexschool.io/doc/#/%E4%BD%BF%E7%94%A8%E8%80%85) - 註冊功能 API: ```html https://todolist-api.hexschool.io/users/sign_up 請求資料格式: { "email": "example@gmail.com", "password": "example", // password 長度需 6 個字 "nickname": "example" } ``` - 登入功能 API: ```html https://todolist-api.hexschool.io/users/sign_in 請求資料格式: { "email": "example@gmail.com", "password": "example" } ``` ## 實作註冊功能 以下將會分三步驟,於 `register.vue` 進行註冊功能的實作說明。 ### 1. 表單結構與 Vue 資料格式設計 根據 API 格式設計 “暱稱”、’信箱”、”密碼” input 與 label 欄位並加入對應的 `for` 與 `id` 屬性。三個欄位都是必填欄位,“暱稱” 欄位提供文字輸入框 ( `type="text"` ) , “信箱” 欄位提供信箱輸入框 ( `type="email"` ) ,“密碼” 欄位提供密碼輸入框 ( `type="password"`) 。 表單結構定義完畢之後,使用 `ref()` 設計具有響應性的 `registrationFormData` 物件 ,並通過 `v-model` 將表單欄位與該物件進行雙向綁定。 表單使用的資料格式 : ```html <script setup> <!-- Nuxt3 Auto-imports 機制有將 ref() 自動引入,不需另外 import { ref } from 'vue'; --> const registrationFormData = ref({ email: "", password: "", nickname: "", }); </script> ``` 在表單中,使用 `v-model` 將輸入的資料綁定到 `registrationFormData`: ```html <template> <form> <div class="mb-4"> <label for="nickName">暱稱<span class="text-danger">*</span></label> <input type="text" class="form-control" id="nickName" placeholder="請輸入您的暱稱" required v-model="registrationFormData.nickname" /> </div> <div class="mb-4"> <label for="email">信箱 <span class="text-danger">*</span></label> <input type="email" class="form-control" id="email" placeholder="example@gmail.com" required v-model="registrationFormData.email" /> </div> <div class="mb-4"> <label for="password">密碼 <span class="text-danger">*</span></label> <input type="password" class="form-control" id="password" placeholder="請輸入密碼" required v-model="registrationFormData.password" /> </div> <div class="d-flex gap-3"> <button class="btn btn-primary w-50" type="submit"> 註冊 </button> <NuxtLink to="/login" class="btn btn-outline-primary w-50" >已經有帳號</NuxtLink > </div> </form> </template> ``` ### 2. 註冊邏輯與 API 請求 宣告 `createUserAccount` 函式,使用 Nuxt3 `$fetch` 實作註冊邏輯。當註冊成功,會顯示 “註冊成功” 訊息並執行 `router.push("/login");` 導向登入頁面;如果註冊失敗,則會在 `catch` 區塊捕捉錯誤,顯示 `error.response._data.message` 中的錯誤訊息。最後無論成功或失敗,在 `finally` 區塊將 `registrationFormData` 資料重置。 除此之外,加入 `isEnabled` 狀態,在註冊請求完成之前禁用 “註冊” 和 “已經有帳號” 按鈕,避免請求未完成時重複點擊。 ```html <script setup> const router = useRouter(); const registrationFormData = ref({ email: "", password: "", nickname: "", }); const isEnabled = ref(false); const createUserAccount = (body) => { isEnabled.value = true; $fetch("https://todolist-api.hexschool.io/users/sign_up", { method: "POST", body, }) .then(() => { alert("註冊成功"); router.push("/login"); }) .catch((error) => { const { message } = error.response._data; // message 有陣列 [] 和字串 "" 兩種回應格式 if (Array.isArray(message)) { alert(message.join("、")); return; } alert(message); }) .finally(() => { registrationFormData.value = {}; // 清空註冊表單 isEnabled.value = false; // 解鎖按鈕 }); }; </script> ``` ### 3. 送出表單 在 `<form>` 標籤註冊 `submit` 事件並加入 `.prevent` [修飾符](https://vuejs.org/guide/essentials/event-handling#event-modifiers),避免提交表單後重新加載頁面。使用者填寫表單後,點擊 “註冊” 按鈕或是鍵盤 Enter 鍵,將 `registrationFormData` 資料傳入 `createUserAccount()`函式,進行註冊請求的處理。在註冊期間,按鈕將被禁用,避免重複請求。 ```html <template> <form @submit.prevent="createUserAccount(registrationFormData)"> <div class="mb-4"> <label for="nickName">暱稱<span class="text-danger">*</span></label> <input type="text" class="form-control" id="nickName" placeholder="請輸入您的暱稱" required v-model="registrationFormData.nickname" /> </div> <div class="mb-4"> <label for="email">信箱 <span class="text-danger">*</span></label> <input type="email" class="form-control" id="email" placeholder="example@gmail.com" required v-model="registrationFormData.email" /> </div> <div class="mb-4"> <label for="password">密碼 <span class="text-danger">*</span></label> <input type="password" class="form-control" id="password" placeholder="請輸入密碼" required v-model="registrationFormData.password" /> </div> <div class="d-flex gap-3"> <button class="btn btn-primary w-50" type="submit" :disabled="isEnabled"> 註冊 </button> <NuxtLink to="/login" class="btn btn-outline-primary w-50" :class="{ disabled: isEnabled }" >已經有帳號</NuxtLink > </div> </form> </template> ``` ## useCookie 在介紹登入功能和寫入 cookie 之前,我們需要先學習在 Nuxt3 操作 cookie 的方法。Nuxt3 內建了組合式函式 `useCookie` ,允許在伺服器端渲染時讀取和寫入 cookie 。在頁面、元件都可以使用 `useCookie()` 建立一個具有響應性 (`RefImpl`) 的物件。 ### 建立方式 ```jsx const cookie = useCookie(name, options) // useCookie(cookie 的名稱,cookie的屬性) ``` `useCookie` 需要帶入兩個參數: 1. name : cookie 的名稱,為字串格式。 2. options:可選的參數,用於設定 cookie 的屬性,如過期時間、路徑等。 接下來將針對以下 options 參數的屬性進行介紹,其餘的屬性可以至 [官方文件](https://nuxt.com/docs/api/composables/use-cookie#options) 閱讀 : - path: 設定允許操作 cookie 的路徑。 ```jsx // ex: useCookie("auth", { path: "/admin", // 可以在 /admin 路徑下取用 cookie }); ``` - expires : 接受 `Date` 物件作為 cookie 的過期時間。 - maxAge : 設定 `Max-Age` 屬性的值,以秒為單位寫入 cookie 的過期時間。。與 `expires` 的差別在於,`expires` 寫入的是 UTC 格式的時間戳記;而 `maxAge` 寫入的是秒數,例如下方範例,`maxAge` 屬性寫入 60,代表 60 秒之後 cookie 就會過期。 ```jsx useCookie("user", { path: "/", maxAge: 60, // 60 秒之後 **cookie** 過期 }); ``` - secure: 設定 cookie 的 `Secure` 屬性 ( [文件](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Cookies#%E9%99%90%E5%88%B6%E5%B0%8D_cookie_%E7%9A%84%E8%A8%AA%E5%95%8F) ) 。預設值為 `false`,當設定為 `true` ,cookie 只能在 HTTPS 加密的狀態下進行傳輸。 ### 寫入 cookie 在使用 `useCookie()` 建立 cookie 之後,還需要將值寫入。可以使用 `.value = "值"` 來設定 cookie 的內容。 如果 cookie 不存在,會自動建立;如果 cookie 已存在,則會覆蓋原有值。例如下方範例,名稱為 “user” 的 cookie 還不存在,所以會新增一個相同名稱的 Cookie ```jsx // 建立 Cookie 的格式 const userCookie = useCookie('user',{ path: "/", maxAge: 60, }) // 寫入 Cookie userCookie.value="bearer 123456"; // 當修改 .value 時,cookie 的值會自動被更新 ``` 建立 Cookie 之後可以開啟開發工具的 Application 查看 Cookie 狀態,如下圖 : ![day14-1](https://hackmd.io/_uploads/r19fYptzyx.png) ### 讀取 cookie 在其它地方 ( 或元件 ) 有讀取 cookie 的需求,可以使用 `useCookie("cookie 的名稱")` 語法,只需要指定 cookie 名稱,不需再次傳入 `options`。例如下方範例,名稱為 “user” 的 cookie 已經存在,所以會新增一個相同名稱的 Cookie ```jsx // 建立 Cookie 的格式 const userCookie = useCookie("user", { // 建立名稱為 user 的 cookie path: "/", maxAge: 600, }); // 寫入 Cookie userCookie.value = "bearer 123456"; /* ------------ */ // 讀取 cookie const getUserCookie = useCookie("user"); // 取得名稱為 “user” 的 cookie console.log(getUserCookie.value) // 會取得 bearer 123456 ``` ### 刪除 cookie 要刪除 cookie,可以將 cookie 的值設為 `null` 或 `undefined` 。 設置值為 `null` 或 `undefined` 會讓瀏覽器刪除該 Cookie,因為它會被視為過期。 ```jsx // 建立 cookie const cartCookie = useCookie("cart", { path: "/", }); // 寫入值 cartCookie.value = "bearer abcde"; // 刪除 cookie cartCookie.value = null; // 或是 // cartCookie.value = undefined; ``` --- ## 實作登入功能 註冊完成後,頁面會轉址到登入頁面 ( `/login` ) 。接下來將分三個步驟來實作 `login.vue` 的登入功能,包含建立表單結構、設計登入邏輯以及綁定表單提交事件。 ### 1. 表單結構與 Vue 資料格式設計 根據 API 格式設計 “信箱” ( `type="email"` ) 、”密碼” ( `type="password"`) input 與 label 欄位,並使用 `ref()` 設計具有響應性的 `loginFormData`物件 ,通過 `v-model` 將表單欄位與該物件進行雙向綁定。 ```html <script setup> const loginFormData= ref({ email: "", password: "", }); </script> <template> <h2 class="h3 mb-4">登入</h2> <form> <div class="mb-4"> <label for="loginemail">信箱 <span class="text-danger">*</span></label> <input type="email" class="form-control" id="loginemail" placeholder="example@gmail.com" required v-model="loginFormData.email" /> </div> <div class="mb-4"> <label for="loginpassword">密碼 <span class="text-danger">*</span></label> <input type="password" class="form-control" id="loginpassword" placeholder="請輸入密碼" required v-model="loginFormData.password" /> </div> <button class="btn btn-secondary w-100" type="submit"> 登入 </button> </form> </template> ``` ### 2. 登入邏輯與存入 Cookie 宣告 `signInUser` 函式實作登入邏輯。在登入成功後,使用 `useCookie()` 來建立名為 `auth` 的 cookie,將 API 回傳的 `token` 寫入該 cookie,並將 API 回傳的有效日期 `exp` 設置為 `expires`。 ```html <script setup> const loginFormData = ref({ email: "", password: "", }); const isEnabled = ref(false); const signInUser = (body) => { isEnabled.value = true; $fetch("https://todolist-api.hexschool.io/users/sign_in", { method: "POST", body, }) .then(({ token, exp }) => { // 設定 cookie,儲存 token 及其過期時間 const cookie = useCookie("auth", { expires: new Date(exp * 1000), path: "/", }); cookie.value = token; alert("登入成功"); }) .catch((error) => { console.dir(error); const { message } = error.response._data; // message 有 陣列 [] 和字串 "" 兩種回應格式 if (Array.isArray(message)) { alert(message.join("、")); return; } alert(message); }) .finally(() => { loginFormData.value = {}; isEnabled.value = false; }); }; </script> ``` ### 3. 送出表單 在 `<form>` 標籤註冊 `submit` 事件並加入 `.prevent` [修飾符](https://vuejs.org/guide/essentials/event-handling#event-modifiers),避免提交表單後重新加載頁面。使用者填寫表單後,點擊 “登入” 按鈕將 `loginFormData`資料傳入 `signInUser()` 函式,進行註冊請求的處理。在註冊期間,按鈕將被禁用,避免重複請求。 ```html <template> <h2 class="h3 mb-4">登入</h2> <form @submit.prevent="signInUser(loginFormData)"> <div class="mb-4"> <label for="loginemail">信箱 <span class="text-danger">*</span></label> <input type="email" class="form-control" id="loginemail" placeholder="example@gmail.com" required v-model="loginFormData.email" /> </div> <div class="mb-4"> <label for="loginpassword">密碼 <span class="text-danger">*</span></label> <input type="password" class="form-control" id="loginpassword" placeholder="請輸入密碼" required v-model="loginFormData.password" /> </div> <button class="btn btn-secondary w-100" :disabled="isEnabled" type="submit"> 登入 </button> </form> </template> ``` <br> > 今日學習的[範例 Code - 資料夾: day14-nuxt3-login-example](https://github.com/hexschool/nuxt-daily-tasks-2024) ## 題目 請 fork 這一份 [模板](https://github.com/jasonlu0525/nuxt3-live-question/tree/day14-nuxt3-login?tab=readme-ov-file) 實作帳號登入功能 : - 作答前需要先在 `/pages/register.vue` 使用模板提供的操作介面註冊帳號,格式如下: - 所有欄位都必填。 - 密碼需要至少 8 碼以上,並英數混合。 - 電話格式可以是手機號碼與市內電話。 - birthday 格式可以是 "yyyy-mm-dd”。 - zipcode 需要對照到各縣市各區的郵遞區號,可以參考 [郵遞區號速查一覽表](https://c2e.ezbox.idv.tw/zipcode.php)。 - 在 `/pages/login.vue` 頁面串接旅館的 [登入 API](https://todolist-api.hexschool.io/doc/#/%E4%BD%BF%E7%94%A8%E8%80%85/post_users_sign_in) ( 需使用 try catch )。登入成功後,使用 `useCookie()` 將 token 寫入名稱為 “auth” 的 cookie。 - 登入成功與失敗皆使用 [sweetAlert2 套件](https://sweetalert2.github.io/) 顯示訊息。sweetAlert2 套件在模板已有安裝與引入,不需再額外設定。 ```jsx $swal.fire({ position: "center", icon: ... , title: ... , showConfirmButton: false, timer: 1500, }); ``` ## 回報流程 將答案上傳至 GitHub 並複製 GitHub repo 連結貼至底下回報就算完成了喔 ! 解答位置請參考下圖(需打開程式碼的部分觀看) ![](https://i.imgur.com/vftL5i0.png) <!-- 解答 : https://github.com/jasonlu0525/nuxt3-live-answer/tree/day14-nuxt3-login --> 回報區 --- | # | Discord | Github / 答案 | | --- | ----- | ----- | | 1 | 眼睛 |[Github](https://github.com/Thrizzacode/nuxt3-live-question/tree/day14-nuxt3-login)| | 2 | Steven |[Github](https://github.com/y7516552/nuxt3-live-question/tree/day14)| | 3 | kevinhes |[Github](https://github.com/kevinhes/nuxt-daily-mission/tree/day14)| | 4 | MY |[Github](https://github.com/ahmomoz/nuxt3-live-question/tree/day14-nuxt3-login-hw)| | 5 | LinaChen |[Github](https://github.com/Lina-SHU/nuxt3-live-question)| | 6 | JimLin |[Github](https://github.com/junhoulin/Nuxt3-hw-day-after10/tree/day14)| | 7 | dragon |[Github](https://github.com/peterlife0617/2024-nuxt-training-homework01/tree/feature/day14)| | 8 | wei_Rio |[Github](https://github.com/wei-1539/nuxtDaily14)| | 9 | Rocky |[Github](https://github.com/WuRocky/Nuxt-Day14-Cookie.git)| | 10 | hsin yu |[Github](https://github.com/dogwantfly/nuxt3-daily-task-live-question/tree/day14-nuxt3-login)| | 11 | tanuki狸 |[Github](https://github.com/tanukili/Nuxt-2024-week01-2/tree/day14-nuxt3-login)| | 12 | barry1104 |[Github](https://github.com/barrychen1104/nuxt3-live-question/tree/day14-nuxt3-login)| | 13 | Ariel |[Github](https://github.com/Ariel0508/nuxtDay14)| | 14 | Johnson |[Github](https://github.com/tttom3669/2024_hex_nuxt_daily/tree/day14-nuxt3-login)| | 15 | lidelin |[Github](https://github.com/Lide/nuxt3-live-question/tree/day14-nuxt3-login)| | 16 | runweiting |[Github](https://github.com/runweiting/2024-NUXT-Hotel)| | 17 | fabio20 |[Github](https://github.com/fabio7621/nuxt3-live-question-d2/tree/day-14-login)| | 18 | 阿塔 |[Github](https://github.com/QuantumParrot/2024-Hexschool-Nuxt-Camp-Daily-Task/blob/main/pages/login.vue)| <!-- | --- | --- |[Github]()| -->