路由轉場動畫其實我沒有在工作中用過,應該是因為我做 B2B 的專案比較多,動畫需求在 toC(面向一般顧客)的情況比較多。但因為有點好玩還有跟前面有連結就一起介紹了,這個部分剛好可以實作路由事件跟路由參數傳遞。
網頁展示:[Day14](https://ithelp-project-production.up.railway.app/pages/day14
可以 Day14 跟 Day9 互相切換,就可以看到動畫效果
首先先來設定動畫的規則:
動畫標記說明:
A=>B
:有 A 標籤的路由,切換到 B 標籤的路由才會觸發A<=>B
:AB 兩個路由互相切換的時候都會觸發*<=>*
:每次切換路由都有同樣動畫(後面也不需要在 layout、route 特別設定)動畫原理:
這個路由動畫主要是透過抓取進入 (:enter
) 和離開 (:leave
) 的元素,來設定動畫進出的點。
export const slideInOut = trigger('routeAnimations', [
// 從 Day9 路由狀態切換到 Day14 路由狀態的動畫
transition('Day9 => Day14', [
// 第1步:設定容器為相對定位,作為絕對定位的參考點
style({ position: 'relative' }),
// 第2步:選取進入(:enter)和離開(:leave)的元素,設定為絕對定位
// :enter = 新進入的頁面 (Day14)
// :leave = 即將離開的頁面 (Day9)
query(':enter, :leave', [
style({
position: 'absolute', // 讓兩個頁面可以重疊
top: 0, // 對齊到容器頂部
left: 0, // 對齊到容器左邊
width: '100%' // 佔滿整個寬度
})
], { optional: true }), // optional: true 避免找不到元素時報錯
// 第3步:設定動畫開始前的初始位置
query(':enter', [
style({ transform: 'translateY(100%)' }) // 新頁面在下方 (螢幕外)
], { optional: true }),
query(':leave', [
style({ transform: 'translateY(0%)' }) // 舊頁面在正常位置
], { optional: true }),
// 第4步:同時執行動畫 (group = 平行執行)
group([
// 舊頁面 (Day9) 向上移出螢幕
query(':leave', [
animate('300ms ease-in-out', style({
transform: 'translateY(-100%)' // 向上移出螢幕
}))
], { optional: true }),
// 新頁面 (Day14) 從下方滑入
query(':enter', [
animate('300ms ease-in-out', style({
transform: 'translateY(0%)' // 移動到正常位置
}))
], { optional: true })
])
]),
// 從 Day14 頁面切換到 Day9 頁面的動畫 (反向)
transition('Day14 => Day9', [
// 第1步:容器設定
style({ position: 'relative' }),
// 第2步:絕對定位設定 (同上)
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
], { optional: true }),
// 第3步:反向的初始位置
query(':enter', [
style({ transform: 'translateY(-100%)' }) // 新頁面在上方 (螢幕外)
], { optional: true }),
query(':leave', [
style({ transform: 'translateY(0%)' }) // 舊頁面在正常位置
], { optional: true }),
// 第4步:反向動畫
group([
// 舊頁面 (Day14) 向下移出螢幕
query(':leave', [
animate('300ms ease-in-out', style({
transform: 'translateY(100%)' // 向下移出螢幕
}))
], { optional: true }),
// 新頁面 (Day9) 從上方滑入
query(':enter', [
animate('300ms ease-in-out', style({
transform: 'translateY(0%)' // 移動到正常位置
}))
], { optional: true })
])
])
]);
在路由設定中,我們需要為每個路由添加動畫標識:
重點說明:
{ animation: 'Day9' }
中的 Day9
只是一個狀態標示符Banana
也可以,但建議有識別性一點的命名 😂{
path: 'day9',
loadComponent: () => import('./day9/day9.component').then(m => m.Day9Component),
data: { animation: 'Day9' }
},
{
path: 'day14',
loadComponent: () => import('./day14/day14.component').then(m => m.Day14Component),
data: { animation: 'Day14' }
}
在 Layout 組件的模板中設定動畫綁定:
重點說明:
[@routeAnimations]
是 Angular 動畫綁定,監聽值的變化來觸發動畫router-outlet
外面那圈prepareRoute()
是自訂的方法,返回值改變時觸發動畫<div class="inner-content" [@routeAnimations]="prepareRoute()">
<router-outlet></router-outlet>
</div>
在 Layout 組件中實作動畫邏輯:
設定步驟:
animations: [slideInOut]
@Component({
selector: 'app-layout',
standalone: true,
imports: [
CommonModule,
NzLayoutModule,
RouterOutlet
],
animations: [slideInOut], // 引入動畫
templateUrl: './layout.component.html',
styleUrl: './layout.component.scss'
})
export class LayoutComponent {
constructor(
private router: Router,
private route: ActivatedRoute,
) {}
prepareRoute(): string {
let route = this.router.routerState.root;
while (route.firstChild) {
route = route.firstChild;
}
const animationData = route.snapshot.data['animation'] || '';
console.log('路由資料:', animationData); // 除錯用
return animationData;
}
}
在 Layout 跟目標組件裡面設定 log,可以發現動畫的作用時間是在 ActivationStart
/ActivationEnd
裡面。
時間軸說明:
在 ActivationStart
/ActivationEnd
這段時間代表:
constructor
會被呼叫NavigationEnd
的時候舊組件才會銷毀詳細流程:
ActivationStart
↓
[RouterOutlet 觸發組件建立]
↓
ComponentFactory.create() // 建立組件實例
↓
constructor 被呼叫 // 組件實例化
↓
組件插入 DOM // 但還沒執行變更偵測
↓
ActivationEnd // 啟用完成訊號
整合前面所學,可以看到動畫系統的架構與流程。Angular 替我們處理了許多細節:
ActivationStart
和 ActivationEnd
會觸發組件切換prepareRoute()
提取路由 data 中的動畫標識:enter
和 :leave
選擇器處理組件三大整合要素:
prepareRoute()
將路由資料轉換為動畫狀態從用戶點擊連結到動畫完成的完整流程:
這個架構的優雅之處在於關注點分離: