---
# System prepended metadata

title: "\U0001F3C5 Day 14 - 登入功能與存入 Cookie"
tags: [2024 Nuxt 直播班]

---

# 🏅 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]()|

-->