Vue3 - 路由系統(Vue Router 4)
主題:路由 Transition 動畫
簡介
在單頁應用(SPA)中,路由切換的過程往往是使用者感受到的第一個交互體驗。若切換瞬間毫無過渡,畫面會顯得突兀,使用者甚至會誤以為程式卡住。相反地,加入適當的 transition 動畫 不僅能緩解這種突兀感,還能提升整體 UI 的流暢度與專業度。
Vue3 搭配 Vue Router 4,提供了極為簡潔的方式在路由切換時套用動畫。只要把 <router-view> 包在 <transition>(或 <transition-group>)裡,配合 CSS 或 JavaScript 的過渡效果,即可輕鬆實作 頁面切換動畫。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領你在 Vue3 專案中加入路由動畫。
核心概念
1. <router-view> 與 <transition> 的基本結合
<router-view> 是 Vue Router 用來渲染當前路由對應組件的佔位符。將它包在 <transition> 中,Vue 會在路由組件 進入(enter) 與 離開(leave) 時自動套用過渡類別。
<!-- App.vue -->
<template>
<div id="app">
<!-- 1. 包在 transition 中 -->
<transition name="fade" mode="out-in">
<router-view />
</transition>
</div>
</template>
<style>
/* 2. CSS transition 定義 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
name="fade":Vue 會自動產生fade-enter-active、fade-enter-from、fade-enter-to、fade-leave-active、fade-leave-from、fade-leave-to這六個類別。mode="out-in":先離開舊頁面,完成後再進入新頁面,避免兩個頁面同時呈現造成閃爍。
2. 多種動畫類型:CSS vs JavaScript
2.1 只用 CSS(最簡單)
如上例,只需要寫 CSS 即可,適合 淡入淡出、滑動、縮放 等簡單效果。
2.2 使用 JavaScript Hook(更彈性)
當動畫需求超出 CSS 能力(例如需要根據路由參數動態計算動畫時間),可以利用 <transition> 的 JavaScript 生命週期鉤子。
<template>
<transition
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<router-view />
</transition>
</template>
<script setup>
function beforeEnter(el) {
// 初始化樣式
el.style.transform = 'translateX(100%)';
}
function enter(el, done) {
// 使用 requestAnimationFrame 觸發瀏覽器重排
requestAnimationFrame(() => {
el.style.transition = 'transform 0.5s ease-out';
el.style.transform = 'translateX(0)';
});
// 動畫結束時呼叫 done
el.addEventListener('transitionend', done);
}
function leave(el, done) {
el.style.transition = 'transform 0.4s ease-in';
el.style.transform = 'translateX(-100%)';
el.addEventListener('transitionend', done);
}
</script>
done必須在動畫結束時呼叫,否則 Vue 會一直等待,導致畫面卡住。- 透過
el.style可以根據 路由 meta 或 螢幕尺寸 動態調整動畫參數。
3. 為每個路由設定不同的動畫
有時候不同頁面需要不同的過渡效果(例如列表頁滑入、詳細頁淡入)。可以把動畫名稱寫在 路由 meta,再在 <transition> 中動態取得。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
import Profile from '@/views/Profile.vue';
const routes = [
{ path: '/', component: Home, meta: { transition: 'fade' } },
{ path: '/about', component: About, meta: { transition: 'slide-left' } },
{ path: '/profile/:id', component: Profile, meta: { transition: 'slide-up' } },
];
export default createRouter({
history: createWebHistory(),
routes,
});
<!-- App.vue -->
<template>
<transition :name="transitionName" mode="out-in">
<router-view v-slot="{ Component, route }">
<!-- 讓 Vue 把 key 綁在路由路徑上,確保每次切換都有重新渲染 -->
<component :is="Component" :key="route.fullPath" />
</router-view>
</transition>
</template>
<script setup>
import { useRoute } from 'vue-router';
import { computed } from 'vue';
const route = useRoute();
const transitionName = computed(() => route.meta.transition || 'fade');
</script>
<style>
/* fade */
.fade-enter-active,
.fade-leave-active { transition: opacity .3s; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
/* slide-left */
.slide-left-enter-active,
.slide-left-leave-active { transition: transform .4s; }
.slide-left-enter-from { transform: translateX(100%); }
.slide-left-leave-to { transform: translateX(-100%); }
/* slide-up */
.slide-up-enter-active,
.slide-up-leave-active { transition: transform .4s; }
.slide-up-enter-from { transform: translateY(100%); }
.slide-up-leave-to { transform: translateY(-100%); }
</style>
v-slot="{ Component, route }"讓我們可以直接取得route物件,避免在setup()中額外watch。:key="route.fullPath"確保每次路由變動時<component>都會重新掛載,觸發過渡。
4. 嵌套路由的動畫
在「主畫面」與「子畫面」同時需要動畫時,分層使用 <transition> 能達到更細緻的控制。
<!-- Dashboard.vue (父層) -->
<template>
<div class="dashboard">
<transition name="slide-left" mode="out-in">
<router-view />
</transition>
</div>
</template>
<!-- DashboardHome.vue (子層) -->
<template>
<transition name="fade" mode="out-in">
<router-view />
</transition>
</template>
- 父層的
<transition>控制 子路由切換(例如左側選單切換),子層再自行加入淡入淡出,形成 多層次動畫。
5. 使用 transition-group 處理列表型路由
如果路由切換時同時要呈現多筆資料(例如搜尋結果列表),<transition-group> 可以讓每筆項目分別動畫。
<template>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.title }}
</li>
</transition-group>
</template>
<style>
.list-enter-active,
.list-leave-active { transition: all .3s ease; }
.list-enter-from { opacity: 0; transform: translateY(20px); }
.list-leave-to { opacity: 0; transform: translateY(-20px); }
</style>
- 這裡的
items可能是根據路由參數(例如搜尋關鍵字)而改變的資料陣列。 - 每筆項目會依序淡入或淡出,提升使用者的視覺追蹤感。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記加 key |
若 <router-view> 沒有 key,Vue 會重用同一個組件實例,導致過渡不觸發。 |
使用 :key="route.fullPath" 或 :key="$route.path"。 |
同時使用 out-in 與 mode="in-out" |
設定衝突會使舊頁面仍在渲染,產生閃爍。 | 只保留一種 mode,根據需求選擇 out-in(先離開)或 in-out(先進入)。 |
CSS 動畫時間與 JavaScript done 不一致 |
若 transition 時間長於 done 呼叫時機,動畫會被中斷。 |
在 JavaScript 鉤子裡使用 el.addEventListener('transitionend', done),或手動 setTimeout(done, duration)。 |
| 過度使用複雜動畫 | 造成頁面渲染卡頓,特別在低階裝置。 | 只在關鍵切換使用較簡單的動畫,保持 duration ≤ 400ms。 |
| 路由 Meta 未同步 | 動畫名稱寫在 meta 卻忘了在 App.vue 讀取,導致全部使用預設動畫。 |
確保 `transitionName = computed(() => route.meta.transition |
最佳實踐
- 保持動畫簡潔:淡入淡出、滑動是最常見且效能最佳的選擇。
- 統一管理動畫時間:在專案根目錄建立
variables.css,統一--transition-duration,避免不同頁面時間不一致。 - 使用
mode="out-in":大多數情況下,先離開再進入能避免內容重疊。 - 針對手機端調整:可在
@media中降低duration,提升流暢度。 - 測試不同瀏覽器:Safari、Edge 在
transform與opacity的硬體加速表現略有差異,務必在多平台測試。
實際應用場景
電商商品列表 → 商品詳情
- 列表使用
slide-left,點擊商品後切換到fade,讓使用者感受到「從列表進入」的層次感。
- 列表使用
表單步驟導覽(Wizard)
- 每一步路由使用
slide-up,切換時同時使用transition-group動畫顯示表單欄位的出現/消失。
- 每一步路由使用
管理後台側邊選單
- 左側選單點擊切換主內容時使用
out-in搭配slide-left,讓畫面感覺像是「抽屜」打開。
- 左側選單點擊切換主內容時使用
搜尋結果即時刷新
- 依據路由參數(
/search?q=xxx)更新items,使用transition-group讓每筆結果淡入,提升使用者對結果變化的感知。
- 依據路由參數(
多語系切換
- 切換語系路由時使用
fade,同時在meta設定transition: 'fade',避免因文字長度差異產生突兀感。
- 切換語系路由時使用
總結
Vue Router 4 為 Vue3 提供了 簡潔且彈性的路由過渡機制。只要把 <router-view> 包在 <transition>(或 <transition-group>)裡,配合 CSS 或 JavaScript 鉤子,就能在路由切換時呈現流暢的動畫效果。透過 路由 meta 動態指定動畫名稱、使用 key 確保每次重新渲染、以及在 嵌套路由 中分層運用 <transition>,可滿足從簡單淡入淡出到複雜多層次動畫的各種需求。
在實務開發中,保持動畫簡潔、統一時間、避免過度渲染 是提升使用者體驗的關鍵。掌握本文的核心概念與範例後,你就能在 Vue3 專案中自如地加入路由動畫,為使用者帶來更具沉浸感的互動體驗。祝開發順利,玩得開心!