# 🏅 Day 15 - middleware 與登入狀態驗證 ## 今日學習目標 - 學習 Nuxt3 匿名、具名、全域 middleware ( 路由中間件 ) 的使用方法 - 學習使用 middleware 驗證登入狀態 ## 前言 在進行頁面導航的過程中,經常需要在進入頁面前先進行一些邏輯處理,例如控制訪問權限或驗證使用者是否登入。在 Vue CLI 與 Vite 專案通常是通過 Vue Router 的[導航守衛 (Navigation Guards)](https://router.vuejs.org/zh/guide/advanced/navigation-guards#%E5%AF%BC%E8%88%AA%E5%AE%88%E5%8D%AB) 在全域、路由、元件中控制路由導航,導航守衛會在訪問頁面前先攔截路由執行自訂的驗證邏輯,依成功與否允許進入該路由頁面。 ```jsx // ex: Vue Router v4 Navigation Guards //全域導航 const router = createRouter({ ... }) router.beforeEach((to, from) => { // 自訂邏輯 }) // 路由導航 const routes = [ { path: '/admin,', component: AdminView.vue, beforeEnter: (to, from) => { // 驗證登入狀態 if(!isAuthenticated){ return { path:'/login' } } return true; }, }, ] // 元件導航 <script setup> beforeRouteEnter (to, from, next) { // 驗證登入狀態 if(!isAuthenticated){ return { path:'/login' } } } </template> ``` 在 Nuxt3 中,可以使用內建的 middleware 來實作路由的導航守衛,在導航到頁面前執行自訂的邏輯。今天將使用下方的驗證登入狀態 API 進行講解。請求帶入資料與成功、失敗的訊息格式可以閱讀 [API 文件](https://todolist-api.hexschool.io/doc/#/%E4%BD%BF%E7%94%A8%E8%80%85/get_users_checkout) 。 ```html https://todolist-api.hexschool.io/users/checkout // 請求的 headers 需要夾帶 Authorization headers:{ Authorization } ``` ## middleware Nuxt3 middleware ( 路由中間件 ) 會在頁面導航之前攔截請求,並根據自訂邏輯執行特定的操作。它提供了匿名、具名、全域 三種建立方式。 ### 匿名 middleware 匿名 middleware 不需要建立檔案,直接在頁面元件內透過 [**`definePageMeta()`**](https://nuxt.com/docs/api/utils/define-page-meta#defining-middleware) 的 `middleware` 屬性設定一個函式 。函式提供了 `to` 和 `from` 兩個參數, `to` 參數代表跳轉後的路由資訊, `from` 參數代表跳轉前的路由資訊。 例如下方範例,直接在 `/pages/index.vue` 頁面使用 `definePageMeta()` 建立匿名 middleware: ```html <!-- pages/index.vue --> <script setup> console.log("before middleware"); definePageMeta({ middleware: (to, from) => { // middleware 屬性是函式 console.log("index=>", { to, from }); }, }); console.log("after middleware"); </script> <template> <h1>Page: index</h1> </template> ``` 當進入 `/` 路由會先觸發 middleware ,然後才會執行其他 `console.log()`。在 `/` 路徑開啟開發工具可以看到,`definePageMeta()` 的 middleware 會先執行,然後才依序執行 `'before middleware’` 和 `'after middleware’`,如下圖。 ![day15-1](https://hackmd.io/_uploads/rJlZ9TtzJl.png) <br> ### 具名 middleware 當需要在多個頁面中使用相同的 middleware 時,使用匿名 middleware 會導致重複定義並且難以維護。因此,使用具名 middleware 是更好的選擇,因為它可以在多個地方重複使用。 建立具名 middleware 需要先建立 middleware 資料夾,在資料夾建立管理 middleware 邏輯的檔案。檔案建立後,在頁面元件使用 `definePageMeta()` 的 middleware 屬性加入中間件的邏輯。以下將示範於 middleware 資料夾建立名稱為 example 的檔案,並在 `/pages/about.vue` 頁面加入這個 middleware 。 #### 步驟一. 建立具名 middleware 檔案 在終端機使用 `npx nuxi add middleware example` 指令建立 middleware 資料夾和 `example.ts` 檔案,如果不使用 TypeScript 可以將檔名改成 `example.js`。這裡的 `example` 就是具名 middleware 的名稱,在頁面引入時需要使用該名稱。 #### 步驟二. 在 middleware 檔案內定義具名 middleware 在 `middleware/example.js` 中,使用 [**defineNuxtRouteMiddleware()**](https://nuxt.com/docs/api/utils/define-nuxt-route-middleware) 函式建立 middleware。 `to` 和 `from` 參數分別提供跳轉後以跳轉前的路由資訊。 ```jsx // middleware/example.js export default defineNuxtRouteMiddleware((to, from) => { // middleware 要執行的 code 寫在這邊 console.log("middleware", to, from); }); ``` #### 步驟三. 在頁面中引用具名 middleware 在 `/pages/about.vue` 中使用 `definePageMeta()` 的 `middleware` 屬性引用具名 middleware。當只使用一個 middleware 時,可以放入字串 (`"example"` ) 或 陣列 ( `["example"]` )。 ```html <!-- pages/about.vue --> <script setup> console.log("before middleware"); definePageMeta({ middleware: "example", // 或是 middleware: ["example"], }); console.log("after middleware"); </script> <template> <h1>Page: about</h1> </template> ``` 如果需要使用多個具名 middleware,可以將 `middleware` 屬性設為陣列格式: ```html <script setup> console.log("before middleware"); definePageMeta({ middleware: ["example", "other-middleware1", "other-middleware2"], }); console.log("after middleware"); </script> <template> <h1>Page: about</h1> </template> ``` > ❗ 注意事項 : 具名 middleware 的檔案名稱需要使用 kebab-case 命名規則,例如 `middleware/other-middleware1.js` 而不是 `middleware/otherMiddleware1.js` 。 <br> 在 `definePageMeta()` 中引用時也必須保持一致,使用 **kebab-case** 格式。 definePageMeta({ middleware: ["example", "other-middleware1", "other-middleware2"], }); <br> 進入 `/about` 頁面之後,會依照陣列填入的順序執行具名 middleware。例如下圖, middleware 順序依次執行為 `example.js` ⇒ `other-middleware1.js` ⇒ `other-middleware2.js`。 ![day15-2](https://hackmd.io/_uploads/H1gzcaYz1g.png) <br> ### 全域 middleware 在具名 middleware 的檔名加入後綴 `.global` 就會變成全域的 middleware。這個路由中間件不需要再填入 `definePageMeta()`,它會被自動載入,並在每次換頁時自動執行。 建立步驟與具名 middleware 第一、二步相同,只需在檔名加入 `.global` 後綴。以 `check.global.js` 為例,建立步驟如下: #### 步驟一. 建立具名 middleware 檔案 在終端機使用 `npx nuxi add middleware check.global` 指令建立 middleware 資料夾和 `check.lobal.js` 。 #### 步驟二. 在 `check.lobal.js` 檔案內定義具名 middleware ,使用方式和具名 middleware 相同。 ```jsx // middleware/check.global.js export default defineNuxtRouteMiddleware((to, from) => { // middleware 要執行的 code 寫在這邊 console.log("global middleware", to, from); }); ``` 進入 `/about` 與 `/` 頁面時,會執行 `check.global.js` 的內容(如下圖)。從圖片可以看到,全域 middleware 的執行順序優先於頁面掛載的 middleware。如果一個頁面有多個具名 middleware,它們會依照定義的順序執行。 例如: - `/about` 頁面執行順序為:全域 ⇒ `example.js` ⇒ `other-middleware1.js` ⇒ `other-middleware2.js` - `/` 頁面執行順序為:全域 ⇒ 匿名的 middleware ![day15-3](https://hackmd.io/_uploads/SkQIq6KMkx.png) <br> ## middleware 實作登入狀態驗證 接下來將接續 Day14 的登入功能,在登入成功後跳轉至 `/todos` 頁面,並在進入 `/todos` 頁面前使用 middleware 攔截頁面的導航,進行登入狀態驗證。 ### 登入頁面調整 首先,在 `pages/login.vue` 中新增登入成功後的頁面跳轉邏輯,使用 `router.push()` 將使用者導向 `/todos` 頁面。 ```jsx $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("登入成功"); router.push("/todos"); // <====================== 跳轉至 /todos 頁面 }) .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; }); ``` ### 定義中間件邏輯 在 `pages/todos.vue` 中使用 `definePageMeta()` 定義中間件邏輯,以匿名 middleware 為例。 #### 步驟一. 完成匿名 middleware 結構 ```html <script setup> definePageMeta({ middleware: () => { // 在裡面執行登入驗證 }, }); </script> ``` #### 步驟二. 檢查 Token 並處理未登入的狀況 使用 `useCookie("auth")` 取出名稱為 "auth" 的 cookie,檢查是否有儲存的 token。若沒有,將使用者導向登入頁面。 ```html <script setup> definePageMeta({ middleware:() => { // 檢查 token 是否有值 const token = useCookie("auth"); if (!token.value) return navigateTo("/login"); } }); </script> ``` #### 步驟三. API 請求驗證 Token 使用 `async/await` 把 token 帶入 `$fetch()` 的參數,發出登入驗證請求。 當請求成功時,`userInfo` 變數會存入包含 `status、uid、nickname` 屬性的物件,可以使用 `userInfo.status` 的布林值判斷是否允許進入頁面。 ```json { "status": true, "uid": "...", "nickname": "..." } ``` 當請求失敗時,會執行 `.catch()`,將回傳的 null 值寫入 `userInfo` 變數。因為沒有 `status` 屬性,所以 `if (userInfo?.status !== undefined)` 判斷為 false,執行函式最下方的 `return navigateTo("/login")` 將頁面導航回登入頁面。 ```jsx definePageMeta({ middleware: async () => { // 檢查 token 是否有值 const token = useCookie("auth"); if (!token.value) return false; const userInfo = await $fetch( "https://todolist-api.hexschool.io/users/checkout", { method: "GET", headers: { Authorization: token.value, }, } ).catch(() => { return null; }); if (userInfo?.status !== undefined) { // 驗證成功,終止函式執行 return; } console.log("isLoggedIn", userInfo); // 驗證失敗,導引回登入頁面 // 在伺服器端和客戶端操作路由 return navigateTo("/login"); }, }); ``` 補充: [navigateTo()](https://nuxt.com/docs/api/utils/navigate-to) 是 Nuxt3 提供的工具函式,在伺服器端和客戶端均可使用。與 Vue3 router 的差異在於, Vue3 router 只能在客戶端使用,若要在伺服器端操作路由就需要改成使用 Nuxt3 的navigateTo()。 函式可以傳入 `to`和 `options`( 可選 ) 兩個參數。 ```jsx navigateTo(to,options) ``` 第一個 `to` 參數是必需的,可以是路由物件或是路由路徑的字串,例如: ```jsx // 路由路徑 navigateTo("/login") // 路由物件 navigateTo({ path: "/login", /* 也可以帶入其他路由參數,如 query query: { ... } */ }); ``` 第二個參數 `options` 物件可以設定路由導航的額外資訊,例如 `redirectCode` (重定向代碼)。 其他參數可以閱讀 [官方文件](https://nuxt.com/docs/api/utils/navigate-to#options-optional) ```jsx navigateTo('/login', { redirectCode: 302 }) ``` #### 步驟四. 避免 `$fetch` 重複發出 API 請求 因為 Nuxt3 [混合渲染模式](https://nuxt.com/docs/guide/concepts/rendering#universal-rendering) 的關係,`$fetch` 發出請求後會在伺服器端打一次 API,在客戶端渲染結束後又會再打一次 API,所以需要在 middleware 發出驗證之前判斷,當程式在客戶端與 `nuxtApp.isHydrating` ( Hydration 渲染階段 ) ,不執行中間件的邏輯: - `import.meta.client`:檢查是否在客戶端環境中執行。 - `nuxtApp.isHydrating`:檢查是否正在將伺服器端渲染的程式碼轉換成可以在客戶端操作的程式碼。 - `nuxtApp.payload.serverRendered`:檢查伺服器端是否已經渲染過 HTML,確認伺服器端的內容是否已經存在。 ``` const nuxtApp = useNuxtApp(); if ( import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered ) { return; } ``` <br> > 今日學習的[範例 Code - 資料夾: day15-nuxt3-middleware-example](https://github.com/hexschool/nuxt-daily-tasks-2024) ## 題目 請 fork 這一份 [模板](https://github.com/jasonlu0525/nuxt3-live-question/tree/day15-nuxt3-middleware?tab=readme-ov-file) ,在 `/pages/login.vue` 與 `/pages/orders.vue` 作答,在登入後檢查帳號是否為登入狀態 : - 在 `/login`頁面登入成功後,使用 router 導航至 `/orders` 頁面。 - 在進入 `/orders` 頁面之前使用名稱為 “auth” 的具名 middleware 驗證登入狀態。 - 驗證登入需使用旅館的 [/api/v1/user/check](https://nuxr3.zeabur.app/swagger/#/Users%20-%20%E4%BD%BF%E7%94%A8%E8%80%85/get_api_v1_user_check) API ,並使用 try catch 捕捉錯誤 。 - 驗證成功,允許進入 `/orders` 頁面。驗證失敗,將路由導航回 `/login` 頁面。 ## 回報流程 將答案上傳至 GitHub 並複製 GitHub repo 連結貼至底下回報就算完成了喔 ! 解答位置請參考下圖(需打開程式碼的部分觀看) ![](https://i.imgur.com/vftL5i0.png) <!-- 解答 : https://github.com/jasonlu0525/nuxt3-live-answer/tree/day15-nuxt3-middleware --> 回報區 --- | # | Discord | Github / 答案 | | --- | ----- | ----- | |1|眼睛|[Github](https://github.com/Thrizzacode/nuxt3-live-question/tree/day15-nuxt3-middleware)| |2|keivnhes|[Github](https://github.com/kevinhes/nuxt-daily-mission/tree/day15)| |3|LinaChen|[Github](https://github.com/Lina-SHU/nuxt3-live-question)| | 4 | Steven |[Github](https://github.com/y7516552/nuxt3-live-question/tree/day15)| | 5 | dragon |[Github](https://github.com/peterlife0617/2024-nuxt-training-homework01/tree/feature/day15)| | 6 | MY |[Github](https://github.com/ahmomoz/nuxt3-live-question/tree/day15-nuxt3-middleware-hw)| | 7 | Rocky |[Github](https://github.com/WuRocky/Nuxt-Day15-middleware.git)| | 8 | wei_Rio |[Github](https://github.com/wei-1539/nuxtDaily14/tree/Day15)| | 9 | Jim Lin |[Github](https://github.com/junhoulin/Nuxt3-hw-day-after10/tree/day15)| |10|hsin yu|[Github](https://github.com/dogwantfly/nuxt3-daily-task-live-question/tree/day15-nuxt3-middleware)| |11|barry1104|[Github](https://github.com/barrychen1104/nuxt3-live-question/tree/day15-nuxt3-middleware)| | 12 | tanuki狸 |[Github](https://github.com/tanukili/Nuxt-2024-week01-2/tree/day15-nuxt3-middleware)| | 13 | Johnson |[Github](https://github.com/tttom3669/2024_hex_nuxt_daily/tree/day15-nuxt3-middleware)| |14|lidelin|[Github](https://github.com/Lide/nuxt3-live-question/tree/day15-nuxt3-middleware)| |15|Ariel|[Github](https://github.com/Ariel0508/nuxt3-hw/tree/day15-nuxt3-middleware)| |16|阿塔|[Github](https://github.com/QuantumParrot/2024-Hexschool-Nuxt-Camp-Daily-Task)| | 17 | runweiting |[Github](https://github.com/runweiting/2024-NUXT-Hotel)| <!-- |---|---|[Github]()| -->