iT邦幫忙

2025 iThome 鐵人賽

DAY 14
3
Modern Web

Angular 進階實務 30天系列 第 14

Day 14:Angular 路由轉場動畫

  • 分享至 

  • xImage
  •  

前言

路由轉場動畫其實我沒有在工作中用過,應該是因為我做 B2B 的專案比較多,動畫需求在 toC(面向一般顧客)的情況比較多。但因為有點好玩還有跟前面有連結就一起介紹了,這個部分剛好可以實作路由事件跟路由參數傳遞。

網頁展示:[Day14](https://ithelp-project-production.up.railway.app/pages/day14
可以 Day14 跟 Day9 互相切換,就可以看到動畫效果


動畫設定 (animations.ts)

首先先來設定動畫的規則:

動畫標記說明:

  • 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 })
        ])
    ])
]);

路由設定 (pages.route.ts)

在路由設定中,我們需要為每個路由添加動畫標識:

重點說明:

  • { 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' }
}

HTML 模板設定 (layout.component.html)

在 Layout 組件的模板中設定動畫綁定:

重點說明:

  • [@routeAnimations] 是 Angular 動畫綁定,監聽值的變化來觸發動畫
  • 作用在包含組件的容器上,要長在 router-outlet 外面那圈
  • prepareRoute() 是自訂的方法,返回值改變時觸發動畫
<div class="inner-content" [@routeAnimations]="prepareRoute()">
  <router-outlet></router-outlet>
</div>

TypeScript 組件設定 (layout.component.ts)

在 Layout 組件中實作動畫邏輯:

設定步驟:

  1. 引入動畫設定 animations: [slideInOut]
  2. 在路由切換完成時,讀取路由的 data 來識別是哪個標籤
@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 裡面。

https://ithelp.ithome.com.tw/upload/images/20250826/20162350Z75p3ytjJw.png

時間軸說明:
ActivationStart/ActivationEnd 這段時間代表:

  1. 守衛通過:已經通過路由守衛了
  2. 組件建立:新組件透過 RouterOutlet 觸發組件建立,開始實例化,並插入 DOM
  3. 資料更新:RouteOutlet 的 activateRouteData (Data 靜態資料) 也已經更新,這時 constructor 會被呼叫
  4. 動畫執行:這時舊組件也還在,可以執行離開動畫,一直到 NavigationEnd 的時候舊組件才會銷毀

詳細流程:

ActivationStart
    ↓
[RouterOutlet 觸發組件建立]
    ↓
ComponentFactory.create()  // 建立組件實例
    ↓
constructor 被呼叫        // 組件實例化
    ↓
組件插入 DOM             // 但還沒執行變更偵測
    ↓
ActivationEnd            // 啟用完成訊號

動畫系統架構與流程

整合前面所學,可以看到動畫系統的架構與流程。Angular 替我們處理了許多細節:

1. 路由層 (Router Layer)

  • 職責:負責導航邏輯和發送路由事件
  • 關鍵事件ActivationStartActivationEnd 會觸發組件切換
  • 特點:完全不知道動畫的存在,只管路由邏輯

2. 協調層 (Coordination Layer)

  • RouterOutlet:核心協調者,連接路由和動畫
  • 變更偵測:機制偵測狀態變化
  • 動畫狀態管理:透過 prepareRoute() 提取路由 data 中的動畫標識
  • 組件容器管理:同時維護新舊組件,讓動畫可以順利執行

3. 動畫層 (Animation Layer)

  • Animation Engine:負責解析和執行動畫
  • Transition 匹配:根據狀態變化匹配對應的 transition
  • 選擇器處理:使用 :enter:leave 選擇器處理組件

關鍵整合點

三大整合要素:

  1. 事件驅動:路由事件觸發整個流程
  2. 狀態傳遞:透過 prepareRoute() 將路由資料轉換為動畫狀態
  3. DOM 同步:新舊組件同時存在,確保動畫平滑過渡

完整時間軸

從用戶點擊連結到動畫完成的完整流程:

  • T1:用戶觸發導航
  • T2:路由事件開始發送
  • T3:RouterOutlet 準備組件切換
  • T4:變更偵測觸發動畫
  • T5:動畫執行視覺過渡
  • T6:清理完成

架構設計的優雅之處

這個架構的優雅之處在於關注點分離

  • 路由系統:只管導航
  • 動畫系統:只管視覺效果
  • RouterOutlet 和變更偵測機制:作為橋樑,讓兩個系統無縫協作

https://ithelp.ithome.com.tw/upload/images/20250826/20162350WGtApmVTCD.png


上一篇
Day 13:Router - Angular Guard
下一篇
Day 15:用 Angular RouteReuseStrategy 打造可切換分頁的功能
系列文
Angular 進階實務 30天18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言