1439 字
7 分鐘

Navigation API 怎麼取代 pushState?SPA 先處理 navigate 與 fallback

SPA 以前常用 clickhistory.pushState()popstate 和一堆例外條件拼出路由。這套做法可以運作,但返回、前進、表單、下載、跨來源連結與程式導覽常常各走一條路,最後變成「某個按鈕有攔截、瀏覽器返回卻沒有」的狀態。

Navigation API 是這個問題的新入口。MDN 將它描述為 History API 與 window.location 在 SPA 場景的後繼方案,並標示為 Baseline 2026;但它不是把 server routing 或初次頁面載入消失。實務上的結論是:navigate 集中處理同源、可攔截的應用程式路由,透過 intercept() 接管渲染,再保留初次載入與不支援瀏覽器的 fallback。

先把幾個責任分開:

需求舊做法常見入口Navigation API 的入口
使用者點同源連結每個 link 自己加 click handlernavigate event
瀏覽器返回/前進popstate同一個 navigation event 流程
程式導覽history.pushState()navigation.navigate()
導覽完成時機自己管理 render promisecommittedfinished
初次載入server 或 hydration仍由 server/既有 bootstrap 負責

這個 API 管理的是瀏覽器的 navigation 與 history 生命週期,不是替你決定 URL 對應哪個元件。路由表、資料取得、快取和畫面更新仍然要由應用程式或框架處理。

先用 feature detection 接上 navigate#

下面是以原生 JavaScript 寫的最小骨架。renderDocument() 代表你的畫面更新函式;實際專案應另外處理 title、focus、scroll restoration、錯誤畫面和快取。

const appNavigation = globalThis.navigation;
if (appNavigation) {
appNavigation.addEventListener('navigate', (event) => {
if (
!event.canIntercept ||
event.hashChange ||
event.downloadRequest ||
event.formData
) {
return;
}
const url = new URL(event.destination.url);
if (url.origin !== location.origin) {
return;
}
event.intercept({
async handler() {
const response = await fetch(url, {
headers: { Accept: 'text/html' },
signal: event.signal,
});
if (!response.ok) {
throw new Error('Navigation failed: ' + response.status);
}
renderDocument(await response.text(), url);
},
});
});
}

event.canIntercept 先排除瀏覽器不允許頁面接管的導覽;hashChange 保留原生的同頁錨點行為;downloadRequestformData 則避免把下載或非 GET 表單錯當成純頁面切換。event.signal 讓 fetch 可以在下一次導覽取代目前請求時中止。

不要把所有 navigate 都攔下來。跨來源連結、下載、檔案上傳和需要完整 document navigation 的情況,讓瀏覽器照原本的方式處理通常更可靠。

intercept() 後要管理兩個完成時機#

event.intercept({ handler }) 接管後,handler 的成功或失敗會影響 navigation 的結果。畫面開始切換與資料真的完成不是同一件事,因此程式導覽可以拆開等待:

const result = globalThis.navigation.navigate('/settings');
result.committed
.then(() => showRouteLoadingState())
.catch((error) => showRouteError(error));
result.finished
.then(() => recordRouteReady())
.catch((error) => recordRouteFailure(error));

committed 適合用來表示 history entry 已經提交、畫面可以進入切換狀態;finished 則適合收尾 metrics、focus 或 loading indicator。兩者都要處理 rejection,否則使用者看到的可能是已經改變 URL,畫面卻停在半完成狀態。

保留 pushState fallback#

Navigation API 雖然已進入 Baseline 2026,專案仍可能需要支援沒有 globalThis.navigation 的執行環境。把程式導覽包在一個小函式中,就能讓新舊路徑共用呼叫端:

function navigateTo(path) {
const target = new URL(path, location.href);
if (globalThis.navigation) {
return globalThis.navigation.navigate(target.href).finished;
}
history.pushState(null, '', target.href);
window.dispatchEvent(new PopStateEvent('popstate'));
return Promise.resolve();
}

fallback 不是把兩套 router 永久並行,而是讓 migration 有一個清楚的邊界:支援 Navigation API 的瀏覽器走 navigate,其他環境保留既有 popstate renderer。當框架本身已經擁有 router 時,應先確認它是否已接管 navigation event,避免在 framework router 外再攔一次。

初次載入和同源限制不能忽略#

Navigation API 不會因為頁面第一次載入就送出 navigate event。使用者直接開啟深層 URL、重新整理或從外部網站進入時,仍需要 server 回傳正確 document,或由既有 hydration/bootstrap 讀取目前 URL。只在 client listener 裡宣告路由,不能取代 SSR、靜態 fallback 或 server rewrite。

另外,這個 API 的可攔截範圍以目前 browsing context 的同源 navigation 為中心,並且是 frame-local。它不會讓頁面任意改寫瀏覽器 history,也不會把跨來源頁面變成同一個 SPA route。實作時至少要保留:

  • server 對每個可分享 URL 的直接回應。
  • 跨來源、下載、檔案上傳和非頁面導覽的原生行為。
  • 同一個 tab、iframe 或 nested browsing context 的路由邊界。
  • route handler 失敗時的 navigateerror、重試與可理解錯誤畫面。

從舊 router 遷移的順序#

若現有程式已經大量使用 pushState,可以分階段移動:

  1. 先把 route table 和 render 函式從 click handler 抽出來。
  2. 將同源的 anchor navigation 接到 navigate,保留原本的 popstate 作為 fallback。
  3. event.canIntercepthashChangedownloadRequestformData 建立拒絕清單。
  4. 將資料請求綁定 event.signal,測試快速連點、返回/前進與請求取消。
  5. 最後才把程式導覽改成 navigation.navigate(),並記錄 committedfinished 的錯誤。

這個順序讓 navigation semantics 先穩定,再替換 API 呼叫。不要一次把 history、資料載入、View Transition 和整個 component lifecycle 同時重寫,否則遇到返回按鈕問題時很難知道是哪一層回歸。

常見問題#

不一定。它在最新瀏覽器已有 Baseline 2026 標示,但專案仍要評估目標瀏覽器、框架 router 和 SSR。先用 feature detection 導入,再保留 fallback,比直接刪掉既有路由層更容易回復。

不會。第一次載入需要 server 或既有 bootstrap 處理;navigate 比較適合接管頁面已經載入後的導覽。

finishedcommitted 為什麼要分開?#

committed 表示 navigation entry 已提交,finished 表示 handler 及後續導覽流程完成。前者適合切換 loading 狀態,後者適合做完成紀錄與收尾;兩者都要處理失敗。

參考資料:

MDN:Navigation API

MDN:Navigation.navigate()

MDN:NavigateEvent.intercept()

Navigation API 怎麼取代 pushState?SPA 先處理 navigate 與 fallback
https://laplusda.com/posts/navigation-api-spa-history/
作者
Zero
發佈於
2026-09-10
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。