iT邦幫忙

2026 iThome 鐵人賽

DAY 24
1
Modern Web

Angular 22 Signal 進化論系列 第 24 篇

Day 24:SPA Router 為什麼不再只靠 History API?

  • 分享至 

  • xImage
  •  

平常使用 Angular Router 時,可以透過 routerLink 或 router.navigate() 切換不同頁面。

模板連結:

<a routerLink="/users">使用者</a>

程式導向:

router.navigate(['/users']);

網址會改變,畫面也會跟著切換,但整個 Angular Application 並不會重新載入。

不過瀏覽器原本的頁面導覽並不是這樣運作的。如果直接修改 window.location:

window.location.href = '/users';

瀏覽器會真正前往新的 URL,離開目前的 Document,再向 Server 請求新的頁面。HTML、JavaScript、CSS 等資源也可能重新載入,Application 也會重新建立。

這種方式很適合傳統的 Multi-Page Application,但 SPA 希望在 URL 改變時保留目前的 Document,只更新需要切換的畫面,因此需要另一種管理 URL 與瀏覽紀錄的方式。

History API 如何讓 SPA 不重新載入

History API 提供幾個常見方法:

// 新增一筆瀏覽紀錄,網址切換成 /users
history.pushState({}, '', '/users');

// 取代目前這筆瀏覽紀錄,不會新增新的 History Entry
history.replaceState({}, '', '/profile');

// 回到上一筆瀏覽紀錄
history.back();

// 前往下一筆瀏覽紀錄
history.forward();

例如:

history.pushState({}, '', '/users');

網址會變成 /users,但目前的 Document 不會重新載入,這也是 SPA Router 能運作的重要基礎。

不過 pushState() 只負責建立新的 History Entry,它不知道 Angular 接下來要顯示哪個 Component。Angular Router 還需要根據目前的 URL 比對 Route,執行對應的導覽並更新畫面。

使用者也可能透過瀏覽器的上一頁或下一頁切換位置。這時 Browser 會改變目前的 History Entry,並觸發 popstate:

window.addEventListener('popstate', () => {
  console.log(location.pathname);
});

Angular Router 監聽到這類變化後,再根據目前的 URL 執行對應的 Navigation。

傳統 SPA Router 便是透過 History API、popstate 與 Router 本身的導覽機制,同時處理程式導向與瀏覽器的上一頁、下一頁。

window.location 與 History API 比較:window.location 會重新載入 Document;pushState() 只修改 URL 與 History,由 Angular Router 負責更新畫面。

History API 管的是瀏覽紀錄,不是完整的 Navigation

History API 可以新增、取代或切換 History Entry,但不會描述 Angular Router 內部的導覽狀態。

例如:

router.navigate(['/users']);

Router 開始處理這次 Navigation 後,中間仍可能被取消,或在載入過程中發生錯誤。

Angular Router 本身就有對應的 Navigation Event:

// Navigation 開始
NavigationStart

// Navigation 成功完成
NavigationEnd

// Navigation 被取消,例如 Guard 阻擋
NavigationCancel

// Navigation 過程發生錯誤
NavigationError

History API 知道瀏覽紀錄發生了變化,卻不知道 Router Navigation 最後是成功、取消還是失敗,這些狀態仍然要由 Router 自己管理。

Navigation API 則把 Navigation 本身帶進 Browser 的原生模型中。

Navigation API

Navigation API 的入口是:

window.navigation

可以直接執行導覽:

navigation.navigate('/users');

也可以監聽 Navigation:

navigation.addEventListener('navigate', event => {
  console.log(event.destination.url);
});

使用者點擊連結、按下 Back / Forward,或程式主動執行 Navigation 時,都可以透過 navigate Event 取得這次導覽的資訊。

History API 著重在 History Entry 的操作;Navigation API 則讓 Browser 能直接掌握一次 Navigation 的發生與狀態。

SPA 可以接手 Browser Navigation

Navigation API 提供 intercept(),讓 Application 可以接手這次導覽。

假設頁面中有一般的連結:

<a href="/users">使用者</a>

正常情況下,Browser 會前往 /users 並載入新的 Document。

透過 Navigation API,可以在 navigate 發生時攔截:

navigation.addEventListener('navigate', event => {
  event.intercept({
    async handler() {
      await renderPage();
    }
  });
});

Browser 仍然知道使用者正在前往 /users,但 Application 可以接手後續處理,在目前的 Document 中更新畫面,不需要重新載入整個頁面。

過去 SPA 通常需要監聽 Link Click、阻止預設行為,再透過 History API 修改 URL;Navigation API 則提供統一的 Navigation Event,讓不同的導覽來源可以從同一套機制處理。

Navigation API 也可以取得目前的 History Entry:

navigation.currentEntry;

或取得目前這個分頁中可存取的同源 History Entries:

navigation.entries();

每一筆都是 NavigationHistoryEntry,包含 URL、索引、識別資訊與 State。

Navigation 的成功與失敗也有對應事件:

navigation.addEventListener('navigatesuccess', () => {
  console.log('Navigation completed');
});

navigation.addEventListener('navigateerror', () => {
  console.log('Navigation failed');
});

使用 navigate() 時,還可以取得這次導覽的兩個階段:

const result = navigation.navigate('/users');

await result.committed;
await result.finished;
  • committed:瀏覽器已經切換到新的 History Entry。
  • finished:這次 Navigation 相關的處理已經完成。

Navigation API 也包含換頁後的 Scroll 與 Focus 行為。如果 Application 想自行控制,可以在 intercept() 中設定:

navigation.addEventListener('navigate', event => {
  event.intercept({
    scroll: 'manual',
    focusReset: 'manual',

    async handler() {
      await renderPage();

      window.scrollTo({
        top: 0
      });

      document.querySelector('h1')?.focus();
    }
  });
});
  • scroll: 'manual':捲動位置改由 Application 自己處理。
  • focusReset: 'manual':不讓 Browser 自動重設焦點。

這些屬於 Browser Navigation 的處理範圍;Angular Application 要顯示哪個畫面,以及 Route 是否能成功啟用,仍然是 Angular Router 的工作。

History API 與 Navigation API 比較:History API 著重 History Entry;Navigation API 多了一層 Navigation Lifecycle,可以知道導覽開始、攔截、完成或失敗,也能處理 Scroll 與 Focus。

Angular Router 開始使用 Browser Navigation API

Navigation API 並不會取代 Angular Router。

Browser 可以處理 URL、History Entry 與 Navigation Lifecycle,但它不知道 Angular 的 Route 設定代表什麼:

{
  path: 'users/:id',
  component: UserDetailComponent
}

Route Matching、Component Activation,以及 Angular Application 內部的導覽規則,仍然由 Router 處理。

Angular 提供:

import {
  provideRouter,
  withExperimentalPlatformNavigation
} from '@angular/router';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      routes,
      withExperimentalPlatformNavigation()
    )
  ]
});

withExperimentalPlatformNavigation() 讓 Angular Router 可以嘗試使用 Browser Navigation API。

這個功能從 Angular 21.1 開始提供,目前在 Angular 22 仍屬於實驗功能,比較適合用來了解 Router 未來可能採用的底層 Navigation 模型,而不是直接導入正式環境。

在連結寫法上也會有所不同:

<!-- 傳統 Angular Router -->
<a routerLink="/users">使用者</a>

<!-- Platform Navigation -->
<a href="/users">使用者</a>

差別在於 Navigation 從哪裡進入 Router。routerLink 直接由 Angular 發起導覽;Platform Navigation 則先由 Browser 辨識這次 Navigation,再交給 Angular Router 處理 Application Routing。

Platform Navigation 與 Router Scroll 的關係

Angular Router 本來就有自己的 Scroll Restoration 機制,withInMemoryScrolling() 並不是 Navigation API 出現後才新增的功能。

例如:

import {
  provideRouter,
  withExperimentalPlatformNavigation,
  withInMemoryScrolling
} from '@angular/router';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      routes,
      withExperimentalPlatformNavigation(),
      withInMemoryScrolling({
        scrollPositionRestoration: 'enabled',
        anchorScrolling: 'enabled'
      })
    )
  ]
});

兩個 Feature 處理的是不同層次:

  • withExperimentalPlatformNavigation():改變 Angular Router 與 Browser Navigation 的整合方式。
  • withInMemoryScrolling():負責 Angular Router 本身的 Scroll Restoration。

scrollPositionRestoration: 'enabled' 會在一般新的 Navigation 後回到頁面頂端,Back / Forward 則恢復先前記錄的捲動位置。

anchorScrolling: 'enabled' 則處理 URL Fragment,例如:

/users#profile

頁面中有:

<section id="profile">
  ...
</section>

Router 可以在導覽後捲到對應的位置。

這裡處理的是整個頁面的 Viewport Scroll。如果畫面裡還有自己的 Scroll Container,例如設定 overflow: auto 的 Table 區塊,裡面的 scrollTop 不會由 withInMemoryScrolling() 自動保存。

在使用 Platform Navigation 時,單次 Navigation 還可以透過 NavigationBehaviorOptions 控制 Scroll:

router.navigate(['/users'], {
  scroll: 'manual'
});

scroll 主要有兩種設定:

  • after-transition:Navigation 完成後,按照 Router 的 Scroll Restoration 設定處理。
  • manual:這次 Navigation 不讓 Router 自動處理 Scroll。

因此 withInMemoryScrolling() 定義 Router 平常怎麼處理 Scroll,而 NavigationBehaviorOptions.scroll 則控制這一次 Navigation 是否交給 Router 自動處理。

例如:

router.navigate(['/users'], {
  scroll: 'manual'
});

這次導覽就不會套用 Router 的 Scroll Restoration,後續的捲動位置改由 Application 自己處理。

Angular Router Scroll Restoration - StackBlitz

Angular Router 與 Navigation API 的關係:Navigation API 負責 Browser Navigation;Angular Router 負責 Angular Application 內部的 Route Matching 與畫面切換。

本日結語

window.location 會讓瀏覽器真正前往新的頁面,而 History API 讓 SPA 可以在不重新載入 Document 的情況下修改 URL,再由 Angular Router 更新對應畫面。

History API 主要處理瀏覽紀錄,完整的 Navigation 狀態仍需要 Router 自己管理。Navigation API 則把 Navigation 帶進 Browser 的原生模型中,讓導覽可以被監聽、攔截,也能取得完成與失敗狀態。

Angular Router 仍然負責 Application 內部的 Routing,而 Browser Navigation、History Entry、Scroll 與 Focus 等行為,則可以和 Web Platform 有更直接的整合。

withExperimentalPlatformNavigation() 目前仍屬於實驗功能,不適合直接當成正式環境的預設方案,但它提供了一個方向:Angular Router 不需要自己包辦所有 Browser Navigation 細節,而是能逐步建立在瀏覽器原生的 Navigation 能力上。

資料來源


上一篇
Day 23:RouteReuseStrategy,切換路由一定要銷毀 Component 嗎?
下一篇
Day 25:Angular SignalStore,原生 Signal 不夠用嗎?
系列文
Angular 22 Signal 進化論 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言