iT邦幫忙

2026 iThome 鐵人賽

DAY 13
2
Modern Web

Angular 22 Signal 進化論系列 第 13 篇

Day 13:從 Router Event 到 Signal

  • 分享至 

  • xImage
  •  

路由狀態管理在前端開發中比較少被單獨拿出來討論,但在 Angular 裡,一次路由導航其實會經過不少階段,也會在過程中發出不同的 Router Event,例如 NavigationStart、NavigationEnd、NavigationCancel 與 NavigationError。

過去要觀察這些狀態,通常會透過 Router.events 取得事件,再搭配 Router.getCurrentNavigation() 讀取正在執行的 Navigation。

隨著 Angular 持續導入 Signal,Router 也開始提供對應的 Signal API。這篇就從導航事件開始,看看過去怎麼處理 Router State,以及現在有哪些狀態可以直接透過 Signal 取得。

路由狀態介紹

在 Angular 中,路由導航不只是把一個網址換成另一個網址。

從使用者點擊連結開始,到新的頁面完成導航,中間可能會經過路由比對、Guard 檢查、Resolver 執行、Lazy Loading 與元件啟用等階段。

Angular Router 會在這些階段發出對應的事件,讓我們知道這次 Navigation 執行到哪個位置。

Angular Router 導航事件流程

一次導航通常會從 NavigationStart 開始,接著進行路由比對。如果有設定 Guard 或 Resolver,也會進入對應的檢查與資料解析流程。

最後會進入其中一種結果:

  • NavigationEnd:導航成功完成
  • NavigationCancel:導航被取消
  • NavigationError:導航過程發生錯誤
  • NavigationSkipped:Router 判斷這次導航不需要重新執行,例如再次導航到相同 URL

實際流程不一定每次都會經過所有階段,例如沒有設定 Guard 或 Resolver 時,就不會進入對應的處理流程。

如果想直接觀察 Router 發出的事件,可以在 provideRouter() 加上 withDebugTracing():

import { ApplicationConfig } from '@angular/core';
import {
  provideRouter,
  withDebugTracing,
} from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withDebugTracing()
    ),
  ],
};

啟用後,導航期間的 Router Event 都會輸出到瀏覽器 Console,方便查看實際執行順序。

withDebugTracing() 主要用於開發與除錯,正式環境通常不需要開啟。

透過 Router.events 監聽路由事件

Angular Router 提供 Router.events,可以取得 Navigation 過程中發出的事件。

import { inject } from '@angular/core';
import {
  NavigationCancel,
  NavigationEnd,
  NavigationError,
  NavigationStart,
  Router,
} from '@angular/router';

export class AppComponent {
  private readonly router = inject(Router);

  constructor() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        console.log('開始導航');
      }

      if (event instanceof NavigationEnd) {
        console.log('導航完成');
      }

      if (event instanceof NavigationCancel) {
        console.log('導航取消');
      }

      if (event instanceof NavigationError) {
        console.log('導航發生錯誤');
      }
    });
  }
}

Router.events 是一個 Observable,會隨著 Navigation 進行持續發出不同類型的 Router Event。

它適合用在需要針對特定事件處理的情境,例如頁面瀏覽紀錄、錯誤追蹤,或觀察 Guard、Resolver 的執行狀態。

如果要顯示頁面切換中的 Loading,也可以在 NavigationStart 時開啟,再於 NavigationEnd、NavigationCancel 或 NavigationError 時關閉。

不過,如果只是想知道「目前是否有 Navigation 正在進行」,就不一定要自己從多個事件整理出狀態。過去可以透過 Router.getCurrentNavigation() 直接讀取目前正在執行的 Navigation。

從 getCurrentNavigation() 到 currentNavigation Signal

Router.getCurrentNavigation() 會回傳目前正在執行的 Navigation:

const navigation = this.router.getCurrentNavigation();

Navigation 進行期間會取得對應的 Navigation 物件,沒有導航正在執行時則會回傳 null。

例如可以用來判斷路由是否正在切換:

const isNavigating =
  this.router.getCurrentNavigation() !== null;

不過 getCurrentNavigation() 只會讀取執行當下的值,本身不具備響應式能力。

當 Router 之後開始或完成另一個 Navigation,前面讀到的結果不會自動更新,因此過去常會搭配 Router.events,在事件發生時重新讀取:

this.router.events.pipe(
  map(() => this.router.getCurrentNavigation() !== null)
);

到了 Angular 20.2,getCurrentNavigation() 已經標記為 Deprecated,改由 currentNavigation Signal 取代。

import { computed, inject } from '@angular/core';
import { Router } from '@angular/router';

export class AppComponent {
  private readonly router = inject(Router);

  readonly isNavigating = computed(
    () => this.router.currentNavigation() !== null
  );
}

currentNavigation 的型別是:

Signal<Navigation | null>

Navigation 開始後會取得這次 Navigation 的資訊,結束後則回到 null。依賴它的 computed() 也會跟著重新計算,不需要再透過 Router Event 觸發讀取。

getCurrentNavigation() 與 currentNavigation 的差異

currentNavigation() 代表的是正在執行的 Navigation。如果導航已經完成,要讀取目前所在的 URL,則可以使用 router.url。

實際觀察 currentNavigation()

可以直接把 currentNavigation() 的內容顯示在 Template 中:

<nav>
  <a routerLink="/home">Home</a>
  |
  <a routerLink="/user">User</a>
</nav>

<hr />

<p>
  Navigation ID:
  {{ router.currentNavigation()?.id ?? '目前沒有導航' }}
</p>

<p>
  前往:
  {{ router.currentNavigation()?.initialUrl?.toString() ?? '-' }}
</p>

<p>
  Trigger:
  {{ router.currentNavigation()?.trigger ?? '-' }}
</p>

<p>
  目前 URL:
  {{ router.url }}
</p>

<router-outlet />

Component 中只需要取得 Router:

import { Component, inject } from '@angular/core';
import {
  Router,
  RouterLink,
  RouterOutlet,
} from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [
    RouterLink,
    RouterOutlet,
  ],
  templateUrl: './app.component.html',
})
export class AppComponent {
  readonly router = inject(Router);
}

一般 Navigation 很快就會完成,因此畫面上的資料可能只會出現很短的時間。

為了方便觀察,可以加上一個故意延遲兩秒的 Guard:

import { CanActivateFn } from '@angular/router';

export const delayGuard: CanActivateFn = async () => {
  await new Promise(
    resolve => setTimeout(resolve, 2000)
  );

  return true;
};

接著把 Guard 加到兩個路由:

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    canActivate: [delayGuard],
  },
  {
    path: 'user',
    component: UserComponent,
    canActivate: [delayGuard],
  },
];

這樣在 /home 與 /user 之間切換時,就能比較清楚地觀察 Navigation 期間的資料。

例如從 /home 前往 /user:

Navigation ID:11
前往:/user
Trigger:imperative
目前 URL:/home

此時 Navigation 還沒有完成,所以 currentNavigation() 已經記錄這次要前往 /user,但 router.url 仍然停留在原本的 /home。

其中顯示的資訊說明如下:

  • id:這次 Navigation 的流水號
  • initialUrl:Navigation 一開始要前往的 URL
  • trigger:Navigation 的觸發方式,例如 imperative 代表透過 routerLink 或 router.navigate() 主動導航;使用瀏覽器上一頁、下一頁時則可能是 popstate

一般導航與加入 Guard 後的 currentNavigation() 變化

這部分可以參考 StackBlitz 範例

等導航完成後,currentNavigation() 會回到 null,而 router.url 則更新成新的 URL:

this.router.currentNavigation();
// null

this.router.url;
// '/user'

用 currentNavigation() 判斷頁面切換

因為 currentNavigation() 只在 Navigation 期間有值,所以也可以直接拿來判斷頁面是否正在切換:

readonly isNavigating = computed(
  () => this.router.currentNavigation() !== null
);

Template 再根據這個狀態決定是否顯示 Loading:

@if (isNavigating()) {
  <app-page-loading />
}

<router-outlet />

這裡的 Loading 代表的是「頁面正在切換」,適合放在 Layout 或 App Shell 層級,和頁面內某支 API 的 Loading 是不同的狀態。

lastSuccessfulNavigation

除了正在進行的 Navigation,Router 也提供 lastSuccessfulNavigation,用來取得最近一次成功完成的 Navigation。

在 Angular 20 中,lastSuccessfulNavigation 還是一般的 Navigation | null;到了 Angular 21,則改成:

Signal<Navigation | null>

例如:

private readonly router = inject(Router);

readonly lastNavigation =
  this.router.lastSuccessfulNavigation;

讀取時直接呼叫 Signal:

const navigation =
  this.router.lastSuccessfulNavigation();

它和 currentNavigation() 關注的時間點不同:

  • currentNavigation():正在執行的 Navigation,結束後會回到 null
  • lastSuccessfulNavigation():最近一次成功完成的 Navigation,會保留到下一次成功導航完成

以從 /users 前往 /orders 為例:

currentNavigation 與 lastSuccessfulNavigation 在導航前後的值

isActive()

除了 Navigation 本身,Angular Router 也提供 Signal-based 的 isActive(),用來判斷指定 URL 是否符合目前路由。

例如側邊選單希望 /users 底下的頁面都維持 Active:

import { inject } from '@angular/core';
import { isActive, Router } from '@angular/router';

export class SidebarComponent {
  private readonly router = inject(Router);

  readonly isUsersActive = isActive(
    '/users',
    this.router
  );
}

isActive() 回傳:

Signal<boolean>

Template 可以直接使用:

<a
  routerLink="/users"
  [class.active]="isUsersActive()"
>
  使用者
</a>

沒有傳入比對選項時,isActive() 的 paths 預設是 'subset',因此 /users/1、/users/2 這類子路由也會算是 Active。

其他預設值則是:

{
  paths: 'subset',
  queryParams: 'subset',
  matrixParams: 'ignored',
  fragment: 'ignored',
}

如果要自己控制比對方式,可以傳入第三個參數:

readonly isUsersActive = isActive(
  '/users',
  this.router,
  {
    paths: 'subset',
    queryParams: 'ignored',
    matrixParams: 'ignored',
    fragment: 'ignored',
  }
);

這幾個選項分別控制 URL 不同部分的比對方式:

  • paths: 'subset':目前 URL 位於 /users 底下就算符合,例如 /users、/users/1、/users/1/edit;改成 'exact' 則需要 Path 完全一致
  • queryParams: 'ignored':忽略 Query Params,因此 /users?page=1 與 /users?page=2 不會影響 Active 結果
  • matrixParams: 'ignored':忽略 Matrix Params,例如 /users;type=admin 中的 type=admin
  • fragment: 'ignored':忽略 Fragment,因此 /users#profile 仍然會視為 /users

過去 Router 也有:

this.router.isActive(...)

但它回傳的是一般的 boolean,只代表執行當下的結果。

新的 isActive() 則回傳 Signal<boolean>,Router State 改變時會重新計算,從 Angular 21.1 起列為正式 API。

isActive() 與 RouterLinkActive

如果只是替導覽連結加上 Active 樣式,原本的 RouterLinkActive 就已經足夠:

<a
  routerLink="/users"
  routerLinkActive="active"
  ariaCurrentWhenActive="page"
>
  使用者
</a>

RouterLinkActive 是 Directive,適合直接在 Template 上處理 class 與 aria-current。

isActive() 則回傳 Signal,因此 Active 結果還可以在 TypeScript 中繼續參與其他狀態推導。

例如根據目前是否位於 /users 底下,決定區塊標題:

readonly sectionTitle = computed(() =>
  this.isUsersActive() ? '使用者管理' : '總覽'
);

當路由位於 /users 或它的子路由時,sectionTitle() 會得到 使用者管理;切換到其他路由後則會重新計算。

isActive() 的 subset 比對,以及與 RouterLinkActive 的選擇

單純處理導覽樣式時,使用 RouterLinkActive 會比較直接;如果 Active 結果還要參與其他程式邏輯,再使用 isActive()。

Router Event 與 Router State

Angular Router 現在可以從「事件」與「狀態」兩個方向處理導航資訊:

  • Router.events:處理 NavigationStart、NavigationEnd、NavigationError 等導航事件
  • currentNavigation():取得正在進行的 Navigation
  • lastSuccessfulNavigation():取得最近一次成功完成的 Navigation
  • isActive():判斷指定 URL 是否符合目前路由

如果需要知道某件事情「什麼時候發生」,例如記錄頁面瀏覽、追蹤 Navigation Error,使用 Router.events 會比較適合。

如果需要的是「目前是什麼狀態」,例如頁面是否正在切換、目前是否位於 /users 底下,就可以直接從對應的 Signal API 取得。

本日結語

Router 開始提供 Signal API 後,改變的不只是 API 寫法,而是部分 Router State 本身變成了可以直接追蹤的響應式狀態。

currentNavigation() 代表正在進行的 Navigation,lastSuccessfulNavigation() 保留最近一次成功完成的 Navigation,而 isActive() 則讓 URL 比對結果也能直接作為 Signal 使用。

Router.events 仍然負責事件發生的時間點;Signal API 則負責讀取 Router 當下的狀態。理解這兩者的差別後,就可以依照實際需求選擇適合的方式,而不需要把所有路由狀態都從 Event Stream 中重新整理一次。

資料來源


上一篇
Day 12:從 httpResource 到 TanStack Query,認識 Server State
系列文
Angular 22 Signal 進化論 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Connie
iT邦新手 2 級 ‧ 2026-09-27 21:53:59

Nice writeup. I just found out about isActive() two days ago.

我要留言

立即登入留言