本文 AI 產出,尚未審核

Vue3 路由系統(Vue Router 4)— meta 屬性與權限控制

簡介

在單頁應用(SPA)中,路由不只是導航的工具,更是實作「權限控管」的第一道防線。Vue Router 4 為每條路由提供了 meta 屬性,讓開發者可以在路由定義內加入自訂資訊(例如:是否需要登入、使用者角色、頁面標題…)。透過在全域守衛 (router.beforeEach) 中判斷 meta,即可在使用者嘗試進入受限頁面時,主動導向登入或錯誤頁面,提升應用的安全性與使用者體驗。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步一步在 Vue3 + Vue Router 4 中完成 meta 屬性與權限控制 的完整流程,適合剛入門的開發者,也能為中階開發者提供可直接套用的實務範本。


核心概念

1. meta 是什麼?

meta 是路由物件的一個可自由擴充的欄位,內容會在路由匹配時一起被保留在 to 參數中。它不會影響路由本身的匹配規則,只是提供「額外描述」的功能。

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Dashboard from '@/views/Dashboard.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { title: '首頁' }               // 只負責顯示頁面標題
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { 
      title: '管理後台',
      requiresAuth: true,               // 必須登入才能進入
      allowedRoles: ['admin', 'editor'] // 允許的使用者角色
    }
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})

2. 為什麼在 meta 放權限資訊?

  • 集中管理:所有與路由相關的權限條件都寫在同一個檔案,方便檢視與維護。
  • 路由守衛可直接取得:在 router.beforeEach 內,to.meta 會自動帶入,省去額外的查詢或狀態管理。
  • 彈性擴充:除了 requiresAuthallowedRoles,還可以加入 breadcrumbkeepAlivelayout 等自訂屬性。

3. 全域前置守衛的運作流程

// src/router/guard.js
import router from './index'
import store from '@/store'   // 假設使用 Pinia/Vuex 管理使用者狀態

router.beforeEach((to, from, next) => {
  // 1. 讀取 meta
  const { requiresAuth, allowedRoles, title } = to.meta

  // 2. 設定文件標題(提升 SEO 與使用者體驗)
  if (title) document.title = `${title} | MyApp`

  // 3. 判斷是否需要登入
  if (requiresAuth && !store.state.user.isLoggedIn) {
    // 未登入 → 導向登入頁,並保留原本想去的路徑
    return next({ name: 'Login', query: { redirect: to.fullPath } })
  }

  // 4. 判斷角色權限(已登入才會檢查)
  if (requiresAuth && allowedRoles) {
    const userRole = store.state.user.role
    if (!allowedRoles.includes(userRole)) {
      // 沒權限 → 導向 403 錯誤頁
      return next({ name: 'Forbidden' })
    }
  }

  // 5. 通過所有檢查 → 正常導向
  next()
})

4. 多層路由與 meta 繼承

在巢狀路由中,子路由會自動繼承父層的 meta(但不會覆寫)。若子路由需要額外條件,只要在子層再加上自己的 meta 即可。

const routes = [
  {
    path: '/admin',
    component: () => import('@/layouts/AdminLayout.vue'),
    meta: { requiresAuth: true, allowedRoles: ['admin'] },
    children: [
      {
        path: '',
        name: 'AdminHome',
        component: () => import('@/views/admin/Home.vue')
      },
      {
        path: 'settings',
        name: 'AdminSettings',
        component: () => import('@/views/admin/Settings.vue'),
        meta: { title: '系統設定' }   // 只補充標題,繼承 requiresAuth & allowedRoles
      }
    ]
  }
]

5. 動態加入或變更 meta

有時候權限資訊會依照 API 回傳結果才決定,此時可以在路由守衛裡 動態修改 to.meta,或在路由定義時使用函式返回 meta

// 動態決定是否需要特別驗證
{
  path: '/profile/:id',
  name: 'UserProfile',
  component: () => import('@/views/UserProfile.vue'),
  meta: route => ({
    title: `使用者 ${route.params.id} 的個人頁`,
    // 只有本人或 admin 才能編輯
    canEdit: store.state.user.id === Number(route.params.id) ||
             store.state.user.role === 'admin'
  })
}

在守衛中讀取:

if (to.meta.canEdit === false) {
  return next({ name: 'Forbidden' })
}

程式碼範例

範例 1:最簡單的登入檢查

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
    return next({ name: 'Login' })
  }
  next()
})

註解:只要 meta.requiresAuthtrue,且 store.getters.isLoggedInfalse,即導向登入頁。


範例 2:角色權限判斷

router.beforeEach((to, from, next) => {
  const { requiresAuth, allowedRoles } = to.meta
  if (requiresAuth) {
    const user = store.state.user
    if (!user.isLoggedIn) {
      return next({ name: 'Login', query: { redirect: to.fullPath } })
    }
    if (allowedRoles && !allowedRoles.includes(user.role)) {
      return next({ name: 'Forbidden' })
    }
  }
  next()
})

註解:先檢查登入狀態,再根據 allowedRoles 判斷是否具備權限。


範例 3:自訂 404 / 403 錯誤頁面

// router/index.js
const routes = [
  // ... 其他路由
  { path: '/403', name: 'Forbidden', component: () => import('@/views/Forbidden.vue') },
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') }
]

// guard.js
router.beforeEach((to, from, next) => {
  // 先處理權限
  // ...
  // 若路由不存在則自動導向 NotFound
  if (!to.matched.length) return next({ name: 'NotFound' })
  next()
})

註解/:pathMatch(.*)* 為 Vue Router 4 的萬用匹配寫法,確保未定義的路由都有友善的錯誤頁面。


範例 4:使用 meta.title 動態設定頁面標題

router.afterEach(to => {
  const baseTitle = 'MyApp'
  document.title = to.meta.title ? `${to.meta.title} - ${baseTitle}` : baseTitle
})

註解afterEach 在路由切換完成後執行,適合更新瀏覽器標題或埋點。


範例 5:在組件內取得權限資訊

<template>
  <div>
    <h1>管理後台</h1>
    <button v-if="canEdit" @click="openEdit">編輯內容</button>
  </div>
</template>

<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
const canEdit = route.meta.canEdit ?? false   // 若未設定則為 false
function openEdit() {
  // 編輯邏輯…
}
</script>

註解useRoute() 讓組件直接存取當前路由的 meta,可根據權限即時調整 UI。


常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
忘記在守衛最後呼叫 next() 若在 beforeEach 中未呼叫 next(),路由會卡住不前進。 務必在每條分支結尾呼叫 next(),或直接回傳 next({ … })
meta 只在路由定義時寫死 動態需求(如根據 API 判斷是否開啟功能)時,靜態 meta 無法滿足。 使用 函式返回 meta 或在守衛內 動態改寫 to.meta
路由守衛過於龐大 所有檢查寫在同一個 beforeEach,可讀性與除錯成本高。 拆分成多個守衛(如 authGuardroleGuard),或使用 router.addRoute 動態註冊受限路由。
權限資訊存放在前端 只靠前端驗證容易被繞過。 後端仍需驗證;前端僅作為使用者體驗的第一層防護。
忘記處理未匹配路由 使用者直接輸入不存在的網址會得到空白頁。 設置 404 路由/:pathMatch(.*)*),並在守衛中檢查 to.matched.length

最佳實踐總結

  1. 統一在 meta 定義權限需求,避免散落在多處程式碼。
  2. 在全域守衛中先處理登入,再處理角色,保持邏輯清晰。
  3. 使用 afterEach 更新頁面標題與 SEO 資訊,提升使用者體驗。
  4. 將錯誤頁面(403/404)列為獨立路由,提供一致的 UI。
  5. 永遠在後端再次驗證,前端僅作輔助。

實際應用場景

  1. 企業內部系統:不同部門(HR、Finance、Admin)擁有不同功能,透過 meta.allowedRoles 直接在路由層級限制。
  2. SaaS 多租戶平台:每個租戶的功能開關在資料庫中設定,載入後在 router.beforeEach 動態調整 to.meta,讓 UI 自動呈現可用功能。
  3. 行動端與桌面端共用程式碼:使用 meta.isMobileOnly 判斷是否僅限手機使用,若在桌面瀏覽則自動導向提示頁。
  4. A/B 測試:在路由 meta 加入 experimentGroup,根據使用者屬性分配不同的 UI/功能,且不需要額外的條件分支。

總結

  • meta 是 Vue Router 4 為路由提供的 彈性資訊容器,非常適合放置 權限需求、頁面標題、功能開關 等自訂屬性。
  • 透過 全域前置守衛 (router.beforeEach) 結合 Pinia/Vuex 的使用者狀態,我們可以在使用者每次切換路由時即時檢查 登入與角色,並在不符合條件時導向 登入、403、404 等友善頁面。
  • 最佳實踐 包含:統一使用 meta、分離守衛邏輯、處理未匹配路由、在 afterEach 更新標題、並且永遠在後端再次驗證。
  • 只要掌握上述概念與範例,即可在 Vue3 專案中快速建立安全、可維護且使用者體驗佳的路由權限機制。

祝你在 Vue3 的旅程中,寫出乾淨、可靠的路由程式碼! 🚀