Vue3 路由系統(Vue Router 4)- 巢狀路由(children)
簡介
在單頁應用(SPA)中,路由 是負責在不同 UI 畫面之間切換的核心機制。Vue Router 4(配合 Vue3)提供了強大的路由功能,其中 巢狀路由(children)讓我們可以在同一個父路由的框架下,呈現多層次、階層式的子畫面。
使用巢狀路由,我們可以:
- 保持 UI 結構一致:父層的版面(如側邊欄、標題列)不必每次切換子頁面時重新渲染。
- 簡化路由設定:把相關的子頁面集中管理,提升可讀性與維護性。
- 支援動態參數、懶載入與路由守衛,在大型專案中尤為重要。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 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 直接取得 |
| 命名視圖渲染錯亂 | components 與 component 混用,或 <router-view name> 拼寫錯誤 |
使用 components(複數)設定多視圖;名稱必須一致 |
| 守衛寫在子路由,導致重複 | 多個子路由需要相同驗證邏輯 | 把守衛搬到父層或使用全域守衛 router.beforeEach |
| 懶載入失效 | import() 放在錯誤位置或忘記加 /* webpackChunkName */(不影響功能但不易除錯) |
確保 component: () => import('...') 放在路由定義內,必要時加註解說明分塊名稱 |
| Breadcrumb(麵包屑)失效 | 巢狀路由的 meta.title 沒有在每層設定 |
為每個子路由都提供 meta: { title: '...' },在 Breadcrumb 組件中遞迴讀取 matched 陣列 |
最佳實踐總結:
- 父層僅負責版面結構,子層只放內容。
- 使用
props: true讓路由參數自動映射至元件屬性,保持元件的可測試性。 - 盡量把共用守衛、懶載入、meta 資訊寫在父層,減少重複。
- 命名視圖 只在需要同時渲染多個區塊時使用,避免過度複雜化路由。
- 測試:在開發階段使用
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 框架(側邊欄、標題列)而不必在每個頁面重新渲染。
- 簡化路由設定,尤其在大型專案中,階層式的路由結構讓程式碼更具可讀性。
- 結合動態參數、路由守衛、懶載入與命名視圖,滿足各種複雜需求。
在實務開發時,請遵守以下 三大原則:
- 把版面結構抽離到父層,子層只關心自己的內容。
- 使用
props: true讓路由參數自然流向元件,避免直接依賴$route。 - 將共用的邏輯(驗證、meta、懶載入)集中於父層或全域守衛,減少重複程式碼。
掌握了巢狀路由的概念與技巧後,你將能在 Vue3 專案中構建更 模組化、可擴充且效能佳 的路由體系,為使用者提供流暢且一致的操作體驗。祝開發順利,玩得開心! 🚀