本文 AI 產出,尚未審核

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. 結合 nextTickafterEach 產生「Loading」效果

雖然本文焦點在 beforeEachbeforeEnter,但常見需求是 在路由切換期間顯示 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 truereturn falsereturn { … }
在同步守衛裡執行非同步操作 直接使用 await 但未把函式標記為 async,會拋錯。 若需要非同步,務必把守衛宣告為 async,或回傳 Promise
過度依賴 localStorage 直接在守衛裡讀 localStorage 會造成 I/O 延遲,且缺乏統一管理。 把驗證邏輯抽成 store / composable,如 Pinia 的 useAuthStore()
beforeEnter 裡改變全域狀態 beforeEnter 只應該決定是否允許導向,避免在此處觸發大量副作用。 將副作用(如載入資料)放在組件的 setuponBeforeMount 中。
忘記清除 nextTick 中的 loading 若路由失敗(被 return false)但仍未關閉 loading,會造成 UI 卡住。 在所有返回路由的分支,確保 isLoading.value = false(可在 router.afterEach 統一處理)。

最佳實踐

  1. 使用 meta 設定權限:把路由需要的資訊(如 requiresAuthroles)放在 meta,保持路由表乾淨且易於維護。
  2. 將驗證邏輯抽離:建立 authpermission 之類的 composable,讓守衛只負責「決策」而非「實作」。
  3. 避免在守衛裡直接呼叫 UI 元件:例如 alert()confirm(),會阻塞 UI。若需要提示,建議使用全域的 dialog store,在守衛中觸發它。
  4. 保持守衛簡潔:每個守衛的職責應該單一,過長的條件判斷會降低可讀性。
  5. 測試路由守衛:使用 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 等工具,可讓專案的安全與使用者體驗同時提升。

掌握了 beforeEachbeforeEnter 的正確寫法,你的 Vue 3 應用將能在 安全、效能與開發效率 三方面取得最佳平衡。祝開發順利,快去把這些守衛寫進你的專案吧! 🚀