Vue 3 路由系統(Vue Router 4)── 路由守衛(beforeEach、beforeEnter)
簡介
在單頁應用(SPA)中,路由守衛是保護頁面、控制導向流程的關鍵機制。
Vue Router 4(配合 Vue 3)提供了全局守衛、路由獨享守衛與組件內守衛三種層級,其中最常使用的就是 beforeEach(全局前置守衛) 與 beforeEnter(路由獨享前置守衛)。
- 全局守衛 會在每一次路由變更時先被觸發,適合放置 登入驗證、權限檢查、全域 loading 等邏輯。
- 路由獨享守衛 僅在特定路由切換時執行,讓你可以針對單一頁面實作更細緻的條件,例如「只有在完成表單填寫後才能進入」等。
掌握這兩種守衛的使用方式,能讓你的 Vue 應用在 安全性、使用者體驗與程式碼可維護性 上都有顯著提升。本文將從概念說明、實作範例、常見陷阱與最佳實踐,一路帶你深入了解路由守衛的每個細節。
核心概念
1. 全局前置守衛 router.beforeEach
beforeEach 會在每一次路由切換(包括首次載入)前被呼叫,接收三個參數:
| 參數 | 型別 | 說明 |
|---|---|---|
to |
RouteLocationNormalized |
即將要進入的目標路由物件 |
from |
RouteLocationNormalized |
目前正離開的來源路由物件 |
next |
Function |
用來控制導向流程的函式(在 Vue Router 4 中可直接回傳值) |
範例 1:簡易登入驗證
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Login from '@/views/Login.vue'
import Dashboard from '@/views/Dashboard.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/login', name: 'Login', component: Login },
{ path: '/dashboard', name: 'Dashboard', component: Dashboard, meta: { requiresAuth: true } },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// 全局前置守衛
router.beforeEach((to, from) => {
const isLoggedIn = !!localStorage.getItem('token') // 假設 token 儲存在 localStorage
// 若目標路由需要驗證,且使用者未登入,導向 login
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
// 已登入卻想去 login,直接導回首頁
if (to.name === 'Login' && isLoggedIn) {
return { name: 'Home' }
}
// 其他情況允許通過
return true
})
export default router
說明
- 使用
meta.requiresAuth標記需要驗證的路由。 router.beforeEach直接回傳一個路由位置物件 ({ name: 'Login' }) 代表「中斷當前導向,改走這個路由」。query.redirect讓登入成功後能自動回到原本想去的頁面。
2. 路由獨享前置守衛 beforeEnter
beforeEnter 只能在路由設定裡使用,僅在匹配到該路由時觸發。它的簽名與 beforeEach 相同,但不會在子路由切換時再次呼叫(除非子路由本身也有 beforeEnter)。
範例 2:防止未完成表單直接離開
// router/index.js
import FormPage from '@/views/FormPage.vue'
const routes = [
// ...其他路由
{
path: '/form',
name: 'Form',
component: FormPage,
beforeEnter: (to, from) => {
// 假設表單狀態保存在 store
const store = useFormStore()
if (store.isDirty && !confirm('表單尚未儲存,確定要離開嗎?')) {
// 返回 false 代表取消本次導航
return false
}
return true
},
},
]
說明
store.isDirty表示表單有未儲存的變更。- 若使用者點擊「取消」,返回
false,Vue Router 會阻止導向。 - 這樣的檢查只在進入
/form時執行,避免每次切換其他路由時都跑這段程式碼。
3. 同步 vs 非同步守衛
守衛可以是同步函式(直接回傳布林或路由物件),也可以是 非同步(返回 Promise 或使用 async/await)。非同步常用於 向後端請求權限資訊。
範例 3:非同步驗證使用者角色
router.beforeEach(async (to, from) => {
// 若路由不需要權限檢查,直接通過
if (!to.meta.roles) return true
try {
const user = await fetchCurrentUser() // 假設返回 { id, role }
const allowed = to.meta.roles.includes(user.role)
if (!allowed) {
// 無權限則導向 403 頁面
return { name: 'Forbidden' }
}
return true
} catch (e) {
// 取得使用者失敗,可能是 token 過期,導向登入
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
說明
to.meta.roles為陣列,例如['admin', 'editor']。fetchCurrentUser可能是 axios 呼叫,回傳使用者資料。- 若發生錯誤(例如 401),自動導向登入頁。
4. 結合 nextTick 與 afterEach 產生「Loading」效果
雖然本文焦點在 beforeEach 與 beforeEnter,但常見需求是 在路由切換期間顯示 loading。可以在全局前置守衛開始 loading,afterEach 結束 loading。
import { ref } from 'vue'
export const isLoading = ref(false)
// router/index.js
router.beforeEach(() => {
isLoading.value = true
})
router.afterEach(() => {
// 確保畫面更新完畢再關閉 loading
nextTick(() => {
isLoading.value = false
})
})
5. 多層守衛的執行順序
| 層級 | 執行順序 |
|---|---|
全域前置守衛 (beforeEach) |
1 |
路由獨享前置守衛 (beforeEnter) |
2 |
| 組件內的 beforeRouteEnter | 3 |
| 組件內的 beforeRouteUpdate | 4 |
| 組件內的 beforeRouteLeave | 5 |
全域後置守衛 (afterEach) |
6 |
了解此順序有助於避免衝突,例如全域守衛已經阻止了導向,則不會再執行後續的路由獨享守衛。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 忘記在守衛中返回值 | 若守衛既不回傳 true/false 也不回傳路由位置,導向會卡住。 |
確保每條分支都有 return true、return false 或 return { … }。 |
| 在同步守衛裡執行非同步操作 | 直接使用 await 但未把函式標記為 async,會拋錯。 |
若需要非同步,務必把守衛宣告為 async,或回傳 Promise。 |
過度依賴 localStorage |
直接在守衛裡讀 localStorage 會造成 I/O 延遲,且缺乏統一管理。 |
把驗證邏輯抽成 store / composable,如 Pinia 的 useAuthStore()。 |
在 beforeEnter 裡改變全域狀態 |
beforeEnter 只應該決定是否允許導向,避免在此處觸發大量副作用。 |
將副作用(如載入資料)放在組件的 setup 或 onBeforeMount 中。 |
忘記清除 nextTick 中的 loading |
若路由失敗(被 return false)但仍未關閉 loading,會造成 UI 卡住。 |
在所有返回路由的分支,確保 isLoading.value = false(可在 router.afterEach 統一處理)。 |
最佳實踐
- 使用
meta設定權限:把路由需要的資訊(如requiresAuth、roles)放在meta,保持路由表乾淨且易於維護。 - 將驗證邏輯抽離:建立
auth、permission之類的 composable,讓守衛只負責「決策」而非「實作」。 - 避免在守衛裡直接呼叫 UI 元件:例如
alert()、confirm(),會阻塞 UI。若需要提示,建議使用全域的 dialog store,在守衛中觸發它。 - 保持守衛簡潔:每個守衛的職責應該單一,過長的條件判斷會降低可讀性。
- 測試路由守衛:使用 Vue Test Utils 搭配 Jest 撰寫單元測試,確保不同使用者狀態下的導向行為正確。
實際應用場景
1. 多層級權限系統
在企業內部系統中,常見「管理員」與「一般使用者」的差異。透過 meta.roles 與全局 beforeEach,可以一次性管控所有需要權限的頁面。
// router/index.js
{
path: '/admin',
component: AdminLayout,
meta: { roles: ['admin'] },
children: [
{ path: 'users', component: UserManagement },
{ path: 'settings', component: SystemSettings },
],
}
全局守衛會根據使用者角色自動阻止非管理員的訪問,並導向「無權限」頁面。
2. 動態載入資料前的權限驗證
在某些情況下,路由對應的頁面需要先從 API 取得資料才能渲染。為避免畫面閃爍或錯誤顯示,可在 beforeEnter 中先行取得資料,成功後才讓路由繼續。
{
path: '/project/:id',
name: 'ProjectDetail',
component: ProjectDetail,
beforeEnter: async (to) => {
const projectId = to.params.id
const data = await api.getProject(projectId)
if (!data) return { name: 'NotFound' }
// 把資料暫存於 route meta,供組件取用
to.meta.projectData = data
return true
},
}
在 ProjectDetail.vue 中:
setup(props, { attrs }) {
const route = useRoute()
const project = ref(route.meta.projectData) // 直接使用 pre‑fetched 資料
// ...
}
3. 防止重複提交與防止惡意刷新
使用 beforeEach 監測 表單提交成功後的導向,若使用者在成功頁面直接刷新或回到表單頁,可以自動導回列表或顯示提示。
router.beforeEach((to, from) => {
if (from.name === 'FormSubmitSuccess' && to.name === 'Form') {
// 防止使用者直接回到舊表單頁
return { name: 'FormList' }
}
return true
})
總結
- 路由守衛 是 Vue Router 4 中不可或缺的「門禁系統」,
beforeEach為全局前置守衛,負責所有路由的共通檢查;beforeEnter為路由獨享前置守衛,讓你針對單一頁面加上細部控制。 - 透過
meta、抽離驗證邏輯、非同步處理,可以寫出 可讀、可測、可維護 的守衛程式碼。 - 注意 返回值、非同步錯誤處理、以及 UI 互動(如 loading、dialog) 的時機,避免常見陷阱。
- 在實務上,守衛常被用於 登入驗證、角色權限、表單防止遺失、資料預載 等情境,配合 Pinia、Composable 等工具,可讓專案的安全與使用者體驗同時提升。
掌握了 beforeEach 與 beforeEnter 的正確寫法,你的 Vue 3 應用將能在 安全、效能與開發效率 三方面取得最佳平衡。祝開發順利,快去把這些守衛寫進你的專案吧! 🚀