Vue Router 路由

Vue Router 是 Vue.js 官方的路由管理器,用於建立單頁應用程式(SPA)。它讓你可以將不同的 URL 對應到不同的元件,實現頁面之間的切換而不需要重新載入整個頁面。

安裝

npm install vue-router@4

基本設定

建立路由

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

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: About
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

註冊路由

// 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')
<!-- App.vue -->
<template>
  <nav>
    <RouterLink to="/">首頁</RouterLink>
    <RouterLink to="/about">關於</RouterLink>
  </nav>

  <!-- 路由匹配的元件會渲染在這裡 -->
  <RouterView />
</template>

路由模式

Hash 模式

URL 會包含 #,如 http://example.com/#/about

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

History 模式(推薦)

URL 看起來像正常的 URL,如 http://example.com/about

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes
})
使用 History 模式需要伺服器配置,確保所有路由都返回 index.html

動態路由

路由參數

const routes = [
  {
    path: '/user/:id',
    name: 'user',
    component: User
  }
]

在元件中存取參數:

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

const route = useRoute()
console.log(route.params.id)
</script>

<template>
  <p>使用者 ID: {{ $route.params.id }}</p>
</template>

多個參數

const routes = [
  {
    path: '/user/:userId/post/:postId',
    component: UserPost
  }
]

可選參數

const routes = [
  {
    // id 是可選的
    path: '/user/:id?',
    component: User
  }
]

響應參數變化

當路由參數變化時(如 /user/1/user/2),元件實例會被重用。使用 watch 來響應變化:

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

const route = useRoute()

watch(
  () => route.params.id,
  (newId) => {
    // 獲取新的使用者資料
    fetchUser(newId)
  }
)
</script>

巢狀路由

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // 預設子路由
        path: '',
        component: UserHome
      },
      {
        path: 'profile',
        component: UserProfile
      },
      {
        path: 'posts',
        component: UserPosts
      }
    ]
  }
]
<!-- User.vue -->
<template>
  <div class="user">
    <h1>使用者 {{ $route.params.id }}</h1>
    <!-- 子路由會渲染在這裡 -->
    <RouterView />
  </div>
</template>

程式化導航

使用 router.push

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

const router = useRouter()

function goToUser(id) {
  // 字串路徑
  router.push('/user/' + id)

  // 帶路徑的物件
  router.push({ path: '/user/' + id })

  // 命名路由
  router.push({ name: 'user', params: { id } })

  // 帶 query
  router.push({ path: '/search', query: { q: 'vue' } })
}
</script>

其他導航方法

// 替換當前歷史記錄(不會在 history 中留下記錄)
router.replace({ path: '/about' })

// 前進/後退
router.go(1)   // 前進一步
router.go(-1)  // 後退一步
router.back()  // 同 router.go(-1)
router.forward() // 同 router.go(1)
<template>
  <!-- 命名路由 -->
  <RouterLink :to="{ name: 'user', params: { id: 123 }}">
    使用者
  </RouterLink>

  <!-- 帶 query -->
  <RouterLink :to="{ path: '/search', query: { q: 'vue' }}">
    搜尋
  </RouterLink>

  <!-- 替換歷史記錄 -->
  <RouterLink to="/about" replace>
    關於
  </RouterLink>

  <!-- 自訂 active class -->
  <RouterLink to="/about" active-class="my-active">
    關於
  </RouterLink>
</template>

路由守衛

全域守衛

// router/index.js
router.beforeEach((to, from) => {
  // to: 即將進入的路由
  // from: 當前路由

  // 檢查是否需要登入
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'login' }
  }

  // 返回 false 取消導航
  // return false

  // 不返回或返回 true 表示允許導航
})

router.afterEach((to, from) => {
  // 導航完成後執行
  document.title = to.meta.title || '預設標題'
})

路由獨享守衛

const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from) => {
      if (!isAdmin()) {
        return { name: 'home' }
      }
    }
  }
]

元件內守衛

<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// 離開前確認
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = confirm('有未儲存的變更,確定要離開嗎?')
    if (!answer) return false
  }
})

// 路由更新時(如參數變化)
onBeforeRouteUpdate((to, from) => {
  // 例如 /user/1 -> /user/2
  fetchUser(to.params.id)
})
</script>

路由 Meta

const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      title: '管理後台',
      roles: ['admin']
    }
  }
]

// 在守衛中使用
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth) {
    // 檢查登入狀態
  }
})

路由懶載入

使用動態 import 實現程式碼分割:

const routes = [
  {
    path: '/',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('../views/About.vue')
  }
]

分組打包

const routes = [
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '../views/Admin.vue'),
    children: [
      {
        path: 'users',
        component: () => import(/* webpackChunkName: "admin" */ '../views/AdminUsers.vue')
      }
    ]
  }
]

滾動行為

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 如果有保存的位置(使用瀏覽器前進/後退)
    if (savedPosition) {
      return savedPosition
    }

    // 如果有 hash,滾動到該元素
    if (to.hash) {
      return { el: to.hash }
    }

    // 預設滾動到頂部
    return { top: 0 }
  }
})

路由過渡動畫

<template>
  <RouterView v-slot="{ Component }">
    <Transition name="fade" mode="out-in">
      <component :is="Component" />
    </Transition>
  </RouterView>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

每個路由不同的過渡

<template>
  <RouterView v-slot="{ Component, route }">
    <Transition :name="route.meta.transition || 'fade'">
      <component :is="Component" />
    </Transition>
  </RouterView>
</template>
const routes = [
  {
    path: '/about',
    component: About,
    meta: { transition: 'slide' }
  }
]

命名視圖

同時渲染多個視圖:

const routes = [
  {
    path: '/',
    components: {
      default: Main,
      sidebar: Sidebar,
      header: Header
    }
  }
]
<template>
  <RouterView name="header" />
  <RouterView name="sidebar" />
  <RouterView /> <!-- 預設 -->
</template>

重定向和別名

重定向

const routes = [
  // 字串
  { path: '/home', redirect: '/' },

  // 命名路由
  { path: '/home', redirect: { name: 'homepage' } },

  // 函式
  {
    path: '/search/:query',
    redirect: (to) => {
      return { path: '/search', query: { q: to.params.query } }
    }
  }
]

別名

const routes = [
  {
    path: '/user',
    component: User,
    alias: '/member' // /member 和 /user 都會渲染 User
  }
]

404 頁面

const routes = [
  // ... 其他路由

  // 捕獲所有不匹配的路由
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]

完整範例

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/Home.vue'),
    meta: { title: '首頁' }
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/Login.vue'),
    meta: { title: '登入' }
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('../views/Dashboard.vue'),
    meta: { requiresAuth: true, title: '儀表板' },
    children: [
      {
        path: '',
        name: 'dashboard-home',
        component: () => import('../views/DashboardHome.vue')
      },
      {
        path: 'settings',
        name: 'settings',
        component: () => import('../views/Settings.vue')
      }
    ]
  },
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('../views/User.vue'),
    meta: { title: '使用者' }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('../views/NotFound.vue'),
    meta: { title: '頁面不存在' }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition
    return { top: 0 }
  }
})

// 全域守衛
router.beforeEach((to, from) => {
  // 設定頁面標題
  document.title = to.meta.title || '我的網站'

  // 檢查登入
  const isLoggedIn = !!localStorage.getItem('token')
  if (to.meta.requiresAuth && !isLoggedIn) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

export default router