本文 AI 產出,尚未審核

Vue3 路由系統(Vue Router 4)- 巢狀路由(children)


簡介

在單頁應用(SPA)中,路由 是負責在不同 UI 畫面之間切換的核心機制。Vue Router 4(配合 Vue3)提供了強大的路由功能,其中 巢狀路由children)讓我們可以在同一個父路由的框架下,呈現多層次、階層式的子畫面。

使用巢狀路由,我們可以:

  1. 保持 UI 結構一致:父層的版面(如側邊欄、標題列)不必每次切換子頁面時重新渲染。
  2. 簡化路由設定:把相關的子頁面集中管理,提升可讀性與維護性。
  3. 支援動態參數、懶載入與路由守衛,在大型專案中尤為重要。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 Vue Router 4 的巢狀路由,適合 初學者到中級開發者 快速上手並套用於實務專案。


核心概念

1. 為什麼需要巢狀路由

在傳統的單層路由中,每一次切換都會重新渲染整個 <router-view>,若有共用的版面(例如 Dashboard 的側邊選單),就必須在每個子路由中重複寫相同的佈局。

巢狀路由 透過在父路由中放置 <router-view>,讓子路由的內容自動渲染於此,父層的 UI 只需要渲染一次,子頁面只負責自己的內容,達到 「外框 + 內部」 的分離。

2. 基本語法

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/components/Layout.vue'          // 父層佈局
import Home from '@/views/Home.vue'                  // 子頁面
import Profile from '@/views/Profile.vue'

const routes = [
  {
    path: '/',
    component: Layout,               // 父層元件
    children: [                      // <-- 巢狀路由定義
      {
        path: '',                    // 空字串代表預設子路由
        name: 'Home',
        component: Home
      },
      {
        path: 'profile',
        name: 'Profile',
        component: Profile
      }
    ]
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})
  • children:陣列內每一個物件都是子路由。
  • 子路由的 path:會自動與父層 path 串接(如 /profile/profile,若父層是 /admin 則變成 /admin/profile)。
  • 父層元件 必須在適當位置放置 <router-view />,子路由的內容才會顯示。

程式碼範例

以下提供 5 個實用範例,涵蓋從最基礎到進階的巢狀路由寫法,並附上詳細註解。

範例一:簡單巢狀結構

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import DashboardLayout from '@/layouts/DashboardLayout.vue'
import Overview from '@/views/dashboard/Overview.vue'
import Settings from '@/views/dashboard/Settings.vue'

const routes = [
  {
    path: '/dashboard',
    component: DashboardLayout,      // 父層:包含側邊欄、標題列
    children: [
      {
        path: '',                    // 預設子路由 → /dashboard
        name: 'DashboardOverview',
        component: Overview
      },
      {
        path: 'settings',           // /dashboard/settings
        name: 'DashboardSettings',
        component: Settings
      }
    ]
  }
]

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

重點DashboardLayout 內部只需要一個 <router-view />,子頁面會自動插入。

<!-- src/layouts/DashboardLayout.vue -->
<template>
  <div class="dashboard">
    <Sidebar />
    <header>Dashboard Header</header>
    <!-- 子路由的內容會渲染在這裡 -->
    <router-view />
  </div>
</template>

範例二:命名視圖(named views)結合 children

有時候父層想同時渲染多個 <router-view>,例如左側的側邊欄與右側的主要內容。這時可以使用 命名視圖

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import MainLayout from '@/layouts/MainLayout.vue'
import HomeMain from '@/views/home/Main.vue'
import HomeSidebar from '@/views/home/Sidebar.vue'

const routes = [
  {
    path: '/',
    component: MainLayout,
    children: [
      {
        path: '',
        components: {                // 注意是 components(複數)
          default: HomeMain,         // 主要區塊對應 <router-view />
          sidebar: HomeSidebar       // 側邊欄對應 <router-view name="sidebar" />
        }
      }
    ]
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})
<!-- src/layouts/MainLayout.vue -->
<template>
  <div class="app">
    <!-- 命名視圖 sidebar -->
    <router-view name="sidebar" />
    <!-- 預設視圖 -->
    <router-view />
  </div>
</template>

技巧:使用命名視圖時,子路由必須使用 components(複數),而非 component


範例三:動態路由參數與巢狀

在 e‑commerce 或部落格等情境下,子路由常需要接受參數(例如商品 ID、文章 slug)。

// src/router/index.js
import ProductLayout from '@/layouts/ProductLayout.vue'
import ProductDetail from '@/views/product/Detail.vue'
import ProductReview from '@/views/product/Review.vue'

const routes = [
  {
    path: '/product/:productId',
    component: ProductLayout,
    props: true,                     // 把路由參數傳給父層元件
    children: [
      {
        path: '',
        name: 'ProductDetail',
        component: ProductDetail,
        props: true                 // 也把參數傳給 Detail 元件
      },
      {
        path: 'review',
        name: 'ProductReview',
        component: ProductReview,
        props: true
      }
    ]
  }
]
<!-- src/views/product/Detail.vue -->
<template>
  <div>
    <h2>商品編號:{{ productId }}</h2>
    <!-- 顯示商品資訊 -->
  </div>
</template>

<script setup>
defineProps({ productId: String })
</script>

注意:若父層與子層都需要同一個參數,props: true 必須在兩層都設定,否則子層不會自動取得。


範例四:路由守衛(navigation guards)在巢狀路由

有時候子頁面需要驗證使用者權限或資料載入完成後才允許進入。Vue Router 允許在 個別子路由 加入 beforeEnter

// src/router/index.js
import AdminLayout from '@/layouts/AdminLayout.vue'
import Users from '@/views/admin/Users.vue'
import Settings from '@/views/admin/Settings.vue'
import { isAdmin } from '@/utils/auth.js'

const routes = [
  {
    path: '/admin',
    component: AdminLayout,
    children: [
      {
        path: 'users',
        name: 'AdminUsers',
        component: Users,
        beforeEnter: (to, from, next) => {
          // 只有管理者才能進入
          if (isAdmin()) next()
          else next({ name: 'Home' })
        }
      },
      {
        path: 'settings',
        name: 'AdminSettings',
        component: Settings,
        // 直接在父層加守衛,子路由自動繼承
        // 這裡示範在子路由寫
        beforeEnter: (to, from, next) => {
          if (isAdmin()) next()
          else next({ name: 'Home' })
        }
      }
    ]
  }
]

最佳實踐:若多個子路由共用相同的驗證邏輯,建議把守衛寫在 父層beforeEnter)或使用全域守衛,以免重複程式碼。


範例五:懶載入(lazy‑loading)與巢狀路由

在大型專案中,路由懶載入 可以顯著減少首屏載入大小。結合巢層結構,只要把子元件以 import() 形式寫入即可。

// src/router/index.js
const routes = [
  {
    path: '/account',
    component: () => import('@/layouts/AccountLayout.vue'), // 父層懶載入
    children: [
      {
        path: '',
        name: 'AccountOverview',
        component: () => import('@/views/account/Overview.vue')
      },
      {
        path: 'orders',
        name: 'AccountOrders',
        component: () => import('@/views/account/Orders.vue')
      },
      {
        path: 'settings',
        name: 'AccountSettings',
        component: () => import('@/views/account/Settings.vue')
      }
    ]
  }
]

小技巧:使用 webpackChunkName 可以自訂分割檔名,方便除錯與快取。

component: () => import(/* webpackChunkName: "account-overview" */ '@/views/account/Overview.vue')

常見陷阱與最佳實踐

常見問題 為何會發生 解決方式/最佳實踐
子路由找不到 父層元件忘記放 <router-view>,或路徑寫錯(缺少 / 確認父層有 <router-view />,路由 path 盡量使用相對路徑(不加 /
路由參數不傳遞 只在父層設 props: true,子層忘記同樣設定 兩層都加 props: true,或在子層使用 route.params 直接取得
命名視圖渲染錯亂 componentscomponent 混用,或 <router-view name> 拼寫錯誤 使用 components(複數)設定多視圖;名稱必須一致
守衛寫在子路由,導致重複 多個子路由需要相同驗證邏輯 把守衛搬到父層或使用全域守衛 router.beforeEach
懶載入失效 import() 放在錯誤位置或忘記加 /* webpackChunkName */(不影響功能但不易除錯) 確保 component: () => import('...') 放在路由定義內,必要時加註解說明分塊名稱
Breadcrumb(麵包屑)失效 巢狀路由的 meta.title 沒有在每層設定 為每個子路由都提供 meta: { title: '...' },在 Breadcrumb 組件中遞迴讀取 matched 陣列

最佳實踐總結

  1. 父層僅負責版面結構,子層只放內容。
  2. 使用 props: true 讓路由參數自動映射至元件屬性,保持元件的可測試性。
  3. 盡量把共用守衛、懶載入、meta 資訊寫在父層,減少重複。
  4. 命名視圖 只在需要同時渲染多個區塊時使用,避免過度複雜化路由。
  5. 測試:在開發階段使用 router.isReady() 或 Vue Devtools 確認 matched 陣列是否符合預期。

實際應用場景

場景 為何適合使用巢狀路由 實作要點
管理後台(Admin Dashboard) 側邊欄、頂部導覽列固定,內容區隨子路由切換 父層 AdminLayout 包含 <Sidebar><Header>,子路由放在 <router-view>
會員中心(Account) 多個子頁面(概覽、訂單、設定)共用相同的外框 使用懶載入減少首次載入大小,props: true 把會員 ID 傳給子頁面
多語系網站 每個語系都有相同的子結構(首頁、關於、聯絡) 父層路由使用語系參數 /en/*/zh/*,子路由保持相同 children 陣列
電商商品詳情 商品頁面需要子頁籤(說明、評論、相關商品) 父層 ProductLayout 包含商品圖片與概覽,子路由切換不同資訊卡
SPA 中的彈出層(Modal) 某些子路由只在特定情境下以 Modal 形式呈現 父層保持主畫面,子路由使用 props: { modal: true } 並在元件內判斷是否以 Modal 包裝

總結

巢狀路由是 Vue Router 4 中提升 SPA 可維護性與使用者體驗的關鍵工具。透過 父層佈局 + 子路由內容 的分離,我們可以:

  • 重複利用 UI 框架(側邊欄、標題列)而不必在每個頁面重新渲染。
  • 簡化路由設定,尤其在大型專案中,階層式的路由結構讓程式碼更具可讀性。
  • 結合動態參數、路由守衛、懶載入與命名視圖,滿足各種複雜需求。

在實務開發時,請遵守以下 三大原則

  1. 把版面結構抽離到父層,子層只關心自己的內容。
  2. 使用 props: true 讓路由參數自然流向元件,避免直接依賴 $route
  3. 將共用的邏輯(驗證、meta、懶載入)集中於父層或全域守衛,減少重複程式碼。

掌握了巢狀路由的概念與技巧後,你將能在 Vue3 專案中構建更 模組化、可擴充且效能佳 的路由體系,為使用者提供流暢且一致的操作體驗。祝開發順利,玩得開心! 🚀