Vue Router 路由管理
2025年5月21日大约 8 分鐘
學習目標
- 了解單頁應用(SPA)路由的概念與原理
- 學會安裝和配置 Vue Router
- 掌握路由定義、導航與參數傳遞
- 理解嵌套路由與路由守衛
- 學會程式化導航與路由元資訊
核心概念解釋
什麼是 Vue Router?
Vue Router 是 Vue.js 的官方路由管理器,用於建構單頁應用(SPA)。它提供:
- 宣告式路由:使用
<RouterLink>
進行導航 - 程式化路由:使用 JavaScript 控制路由跳轉
- 動態路由匹配:支援參數與通配符
- 嵌套路由:支援複雜的頁面結構
- 路由守衛:控制導航權限與流程
路由的工作原理
在 SPA 中,路由通過改變 URL 的 hash 或使用 HTML5 History API 來實現頁面切換,而不會重新載入整個頁面。
程式碼範例及詳細註釋
1. 安裝與基本設定
首先安裝 Vue Router:
npm install vue-router@4
創建路由配置檔案:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 導入頁面組件
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import Contact from '@/views/Contact.vue'
// 定義路由規則
const routes = [
{
path: '/', // 路徑
name: 'Home', // 路由名稱
component: Home, // 對應的組件
meta: { // 路由元資訊
title: '首頁',
requiresAuth: false // 是否需要授權
}
},
{
path: '/about',
name: 'About',
component: About,
meta: {
title: '關於我們'
}
},
{
path: '/contact',
name: 'Contact',
// 懶載入:只有在需要時才載入組件
component: () => import('@/views/Contact.vue'),
meta: {
title: '聯絡我們'
}
}
]
// 創建路由實例
const router = createRouter({
// 使用 HTML5 History API
history: createWebHistory(),
routes,
// 滾動行為設定
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition // 回到之前的滾動位置
} else {
return { top: 0 } // 滾動到頂部
}
}
})
export default router
在主應用中註冊路由:
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 使用路由
app.use(router)
app.mount('#app')
2. 基本路由組件
創建主應用布局:
<!-- src/App.vue -->
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { watch } from 'vue'
const router = useRouter()
const route = useRoute()
// 監聽路由變化,更新頁面標題
watch(
() => route.meta.title,
(newTitle) => {
if (newTitle) {
document.title = `${newTitle} - 我的網站`
}
},
{ immediate: true }
)
</script>
<template>
<div id="app">
<!-- 導航欄 -->
<nav class="navbar">
<div class="nav-brand">
<RouterLink to="/" class="brand-link">我的網站</RouterLink>
</div>
<ul class="nav-menu">
<li>
<!-- RouterLink 自動添加 active class -->
<RouterLink to="/" class="nav-link">首頁</RouterLink>
</li>
<li>
<RouterLink to="/about" class="nav-link">關於</RouterLink>
</li>
<li>
<RouterLink to="/contact" class="nav-link">聯絡</RouterLink>
</li>
</ul>
</nav>
<!-- 路由出口:顯示匹配的組件 -->
<main class="main-content">
<RouterView />
</main>
</div>
</template>
<style scoped>
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: #2c3e50;
color: white;
}
.nav-brand .brand-link {
font-size: 1.5rem;
font-weight: bold;
color: white;
text-decoration: none;
}
.nav-menu {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 2rem;
}
.nav-link {
color: #ecf0f1;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav-link:hover {
background-color: #34495e;
}
/* Vue Router 自動添加的活躍狀態 class */
.nav-link.router-link-active {
background-color: #3498db;
color: white;
}
.main-content {
padding: 2rem;
min-height: calc(100vh - 80px);
}
</style>
3. 動態路由與參數
配置動態路由:
// src/router/index.js 添加動態路由
const routes = [
// ... 其他路由
// 動態路由:用戶個人頁面
{
path: '/user/:id', // :id 是動態參數
name: 'UserProfile',
component: () => import('@/views/UserProfile.vue'),
meta: {
title: '用戶資料'
}
},
// 多個參數
{
path: '/article/:category/:slug',
name: 'Article',
component: () => import('@/views/Article.vue'),
meta: {
title: '文章詳情'
}
},
// 可選參數
{
path: '/products/:category?', // ? 表示可選
name: 'Products',
component: () => import('@/views/Products.vue')
}
]
使用路由參數的組件:
<!-- src/views/UserProfile.vue -->
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const user = ref(null)
const loading = ref(false)
// 獲取用戶資料
const fetchUser = async (userId) => {
loading.value = true
try {
// 模擬 API 呼叫
await new Promise(resolve => setTimeout(resolve, 1000))
user.value = {
id: userId,
name: `用戶 ${userId}`,
email: `user${userId}@example.com`,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${userId}`
}
} catch (error) {
console.error('獲取用戶資料失敗:', error)
} finally {
loading.value = false
}
}
// 監聽路由參數變化
watch(
() => route.params.id,
(newId) => {
if (newId) {
fetchUser(newId)
}
},
{ immediate: true }
)
// 導航到其他用戶
const goToUser = (userId) => {
router.push(`/user/${userId}`)
}
onMounted(() => {
console.log('路由參數:', route.params)
console.log('查詢參數:', route.query)
})
</script>
<template>
<div class="user-profile">
<h1>用戶資料</h1>
<!-- 載入狀態 -->
<div v-if="loading" class="loading">
載入中...
</div>
<!-- 用戶資料 -->
<div v-else-if="user" class="user-card">
<img :src="user.avatar" :alt="user.name" class="avatar">
<div class="user-info">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>用戶ID: {{ user.id }}</p>
</div>
</div>
<!-- 導航到其他用戶 -->
<div class="user-navigation">
<h3>瀏覽其他用戶</h3>
<button
v-for="id in [1, 2, 3, 4, 5]"
:key="id"
@click="goToUser(id)"
:class="{ active: route.params.id == id }"
class="user-btn"
>
用戶 {{ id }}
</button>
</div>
</div>
</template>
<style scoped>
.user-profile {
max-width: 600px;
margin: 0 auto;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.user-card {
display: flex;
align-items: center;
gap: 1rem;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 2rem;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
}
.user-navigation {
margin-top: 2rem;
}
.user-btn {
margin: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #3498db;
background: white;
color: #3498db;
border-radius: 4px;
cursor: pointer;
}
.user-btn.active {
background: #3498db;
color: white;
}
</style>
4. 嵌套路由
定義嵌套路由結構:
// src/router/index.js
const routes = [
// ... 其他路由
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: {
requiresAuth: true
},
children: [
// 空路徑表示預設子路由
{
path: '',
name: 'DashboardHome',
component: () => import('@/views/dashboard/DashboardHome.vue')
},
{
path: 'profile',
name: 'DashboardProfile',
component: () => import('@/views/dashboard/Profile.vue')
},
{
path: 'settings',
name: 'DashboardSettings',
component: () => import('@/views/dashboard/Settings.vue')
},
{
path: 'reports',
name: 'DashboardReports',
component: () => import('@/views/dashboard/Reports.vue')
}
]
}
]
父路由組件:
<!-- src/views/Dashboard.vue -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
<template>
<div class="dashboard">
<div class="dashboard-header">
<h1>控制台</h1>
</div>
<div class="dashboard-layout">
<!-- 側邊導航 -->
<nav class="dashboard-nav">
<ul>
<li>
<RouterLink
to="/dashboard"
class="nav-item"
:class="{ active: route.name === 'DashboardHome' }"
>
總覽
</RouterLink>
</li>
<li>
<RouterLink to="/dashboard/profile" class="nav-item">
個人資料
</RouterLink>
</li>
<li>
<RouterLink to="/dashboard/settings" class="nav-item">
設定
</RouterLink>
</li>
<li>
<RouterLink to="/dashboard/reports" class="nav-item">
報告
</RouterLink>
</li>
</ul>
</nav>
<!-- 子路由出口 -->
<main class="dashboard-content">
<RouterView />
</main>
</div>
</div>
</template>
<style scoped>
.dashboard {
height: 100vh;
display: flex;
flex-direction: column;
}
.dashboard-header {
background: #2c3e50;
color: white;
padding: 1rem 2rem;
}
.dashboard-layout {
flex: 1;
display: flex;
}
.dashboard-nav {
width: 250px;
background: #ecf0f1;
padding: 1rem;
}
.dashboard-nav ul {
list-style: none;
padding: 0;
margin: 0;
}
.nav-item {
display: block;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
color: #2c3e50;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav-item:hover {
background: #bdc3c7;
}
.nav-item.router-link-active {
background: #3498db;
color: white;
}
.dashboard-content {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
</style>
5. 程式化導航
實現程式化路由控制:
<!-- src/components/NavigationExample.vue -->
<script setup>
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const userId = ref('')
const searchQuery = ref('')
// 基本導航
const goHome = () => {
router.push('/')
}
const goBack = () => {
router.back()
}
const goForward = () => {
router.forward()
}
// 帶參數導航
const goToUser = () => {
if (userId.value) {
router.push(`/user/${userId.value}`)
}
}
// 帶查詢參數導航
const searchProducts = () => {
router.push({
path: '/products',
query: {
search: searchQuery.value,
category: 'electronics'
}
})
}
// 使用命名路由導航
const goToArticle = () => {
router.push({
name: 'Article',
params: {
category: 'tech',
slug: 'vue-router-guide'
}
})
}
// 替換當前路由(不會在歷史記錄中留下記錄)
const replaceRoute = () => {
router.replace('/about')
}
// 監聽導航
router.beforeEach((to, from, next) => {
console.log('導航到:', to.path)
console.log('來自:', from.path)
next()
})
</script>
<template>
<div class="navigation-example">
<h2>程式化導航範例</h2>
<div class="nav-section">
<h3>基本導航</h3>
<button @click="goHome">回首頁</button>
<button @click="goBack">上一頁</button>
<button @click="goForward">下一頁</button>
<button @click="replaceRoute">替換到關於頁面</button>
</div>
<div class="nav-section">
<h3>參數導航</h3>
<div class="input-group">
<input
v-model="userId"
placeholder="輸入用戶ID"
type="number"
>
<button @click="goToUser">前往用戶頁面</button>
</div>
</div>
<div class="nav-section">
<h3>查詢參數導航</h3>
<div class="input-group">
<input
v-model="searchQuery"
placeholder="搜索關鍵字"
>
<button @click="searchProducts">搜索產品</button>
</div>
</div>
<div class="nav-section">
<h3>命名路由導航</h3>
<button @click="goToArticle">查看技術文章</button>
</div>
<div class="current-route">
<h3>當前路由資訊</h3>
<pre>{{ JSON.stringify({
path: route.path,
name: route.name,
params: route.params,
query: route.query
}, null, 2) }}</pre>
</div>
</div>
</template>
<style scoped>
.navigation-example {
max-width: 600px;
margin: 0 auto;
}
.nav-section {
margin-bottom: 2rem;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.input-group {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 0.5rem 1rem;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.current-route {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
}
pre {
background: white;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
</style>
6. 路由守衛
實現路由級別的權限控制:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 模擬的授權檢查
const isAuthenticated = () => {
// 實際應用中會檢查 token 或 session
return localStorage.getItem('authToken') !== null
}
const routes = [
// ... 路由定義
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 全域前置守衛
router.beforeEach((to, from, next) => {
console.log(`導航從 ${from.path} 到 ${to.path}`)
// 檢查是否需要授權
if (to.meta.requiresAuth && !isAuthenticated()) {
// 重導向到登入頁面
next({
path: '/login',
query: { redirect: to.fullPath } // 保存原始目標頁面
})
} else {
next() // 繼續導航
}
})
// 全域後置鉤子
router.afterEach((to, from) => {
// 更新頁面標題
if (to.meta.title) {
document.title = `${to.meta.title} - 我的網站`
}
// 發送頁面瀏覽分析
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.path
})
}
})
export default router
組件內守衛:
<!-- src/views/Profile.vue -->
<script setup>
import { ref, onBeforeRouteLeave } from 'vue-router'
const formData = ref({
name: '',
email: ''
})
const isFormDirty = ref(false)
// 監聽表單變化
const updateForm = () => {
isFormDirty.value = true
}
// 組件內路由守衛:離開前確認
onBeforeRouteLeave((to, from, next) => {
if (isFormDirty.value) {
const answer = window.confirm('你有未保存的變更,確定要離開嗎?')
if (answer) {
next()
} else {
next(false) // 取消導航
}
} else {
next()
}
})
</script>
<template>
<div>
<h2>個人資料編輯</h2>
<form @input="updateForm">
<input v-model="formData.name" placeholder="姓名">
<input v-model="formData.email" placeholder="電子郵件">
<button type="submit">保存</button>
</form>
</div>
</template>
補充資源
總結
Vue Router 是建構單頁應用的核心工具。通過本次學習,你應該能夠:
- 理解 SPA 路由的工作原理
- 配置和使用基本路由功能
- 實現動態路由和參數傳遞
- 構建複雜的嵌套路由結構
- 使用路由守衛控制訪問權限
- 掌握程式化導航技巧
掌握這些技能將幫助你建立更專業和用戶友好的 Vue 應用!