題幹與適用場景
如果要把一個 SPA 的導航改造成可恢復、可觀測且不破壞瀏覽器歷史的實作,你會如何使用 Navigation API?請說明載入競態、錯誤回退、捲動恢復和不支援該 API 時的方案。
這道題適合前端、全端和 Web 平台職位。它考察瀏覽器導航模型、非同步狀態機和漸進增強,而不是記住某個框架的路由設定。Navigation API 用一個統一入口觀察同一文件內的導航,並提供 navigate 事件、intercept()、navigatesuccess 和 navigateerror 等能力;它不能取代首屏伺服器渲染,也不能改變跨文件導航的安全邊界。
面試官考察點
- 是否把 URL 和歷史記錄作為導航狀態來源,而不是只依賴元件內部狀態。
- 是否區分同文件路由、跨文件連結、下載、表單提交和跨網域跳轉。
- 是否處理使用者連續點擊造成的過期請求、取消、錯誤和回退。
- 是否說明捲動位置、頁面狀態和瀏覽器前進後退的恢復策略。
- 是否保留首屏渲染和不支援 Navigation API 瀏覽器的降級路徑。
- 是否能用可觀測事件定義成功、失敗、逾時和回滾,而不是只說「切換元件」。
30 秒回答框架
「我先保留伺服器首屏和普通連結,把 URL 與歷史記錄作為唯一導航事實來源。支援 Navigation API 時,只攔截同源、同文件且屬於應用路由的導航,在 navigate 事件中啟動帶取消訊號的載入;新導航到來就取消舊任務,只有目前任務能提交介面。成功後恢復捲動和頁面狀態,失敗則顯示可重試狀態並保留原頁面。瀏覽器不支援時繼續使用既有 History API 路徑,所有路徑共用同一載入器和監控指標。」
分步深入解答
第一步:劃定可以攔截的導航
先檢查目標 URL 是否同源、是否屬於應用路由、是否需要下載或完整文件回應。外部連結、檔案下載、跨網域跳轉、特殊協定和表單語意不應被 SPA 接管。首屏請求仍由伺服器和瀏覽器完成,navigate 事件不能補足重新整理後沒有應用執行階段的問題。
在 navigate 事件中只對安全的應用路由呼叫 intercept()。攔截處理器負責取得資料、更新視圖和恢復捲動;不符合條件時讓瀏覽器執行預設導航。這樣既保留可存取性,也避免把平台導航強行變成客戶端狀態。
第二步:把導航實作成可取消的狀態機
每次導航產生遞增序號,並把 AbortSignal 傳給資料載入器。使用者從 /search?q=a 很快點到 /search?q=ab 時,前一個請求即使晚回傳,也不能覆蓋後一個結果。提交前同時檢查訊號未取消、序號仍是最新值、目標 URL 與目前狀態一致。
let latestNavigation = 0;
navigation.addEventListener("navigate", (event) => {
if (!event.canIntercept || !isAppRoute(event.destination.url)) return;
const id = ++latestNavigation;
event.intercept({
async handler() {
const data = await loadRoute(event.destination.url, event.signal);
if (event.signal.aborted || id !== latestNavigation) return;
renderRoute(data);
}
});
});範例只表達競態原則。真實實作還要處理載入器拋錯、逾時、快取命中和元件卸載。不要用「最後回傳的請求獲勝」,因為網路完成順序不代表使用者最後一次意圖。
第三步:提交歷史、捲動和頁面狀態
導航成功後,以目標 URL 作為提交結果。需要把可恢復的小型狀態寫入目前歷史項目時,可以使用 updateCurrentEntry();大型物件和敏感資料不應塞進歷史狀態。捲動策略要區分新頁面、前進後退和錨點跳轉:新路由通常捲到頂部,返回歷史項目則恢復保存的位置。
不要在載入開始時就修改標題、選中項和麵包屑,除非這些變化有明確的待處理狀態。成功事件後再提交主要視覺狀態,失敗時保留原內容並給出重試入口。navigatesuccess 與 navigateerror 可以作為統一的耗時、取消率和失敗率觀測點。
第四步:處理錯誤、回退和恢復
載入失敗時,原頁面仍應可用;錯誤區域要包含重試、返回和重新載入等下一步動作。對權限失效、資源不存在和網路暫時不可用分別處理,不要把所有異常改寫成 404。若目標頁面需要完整文件回應,則放棄攔截,讓瀏覽器走預設導航。
當應用狀態損壞或客戶端腳本載入失敗,普通連結和伺服器路由仍應能開啟頁面。路由資料要可從 URL 重建,不能只依賴記憶體快取。恢復流程應記錄目標 URL、導航序號、錯誤類型和是否發生回退,便於定位「使用者看到舊頁面但位址已改變」的問題。
第五步:設計相容性和漸進增強
Navigation API 在較新的瀏覽器中可用,但產品不能把它當成唯一入口。先偵測 window.navigation 和所需事件、方法,再選擇增強路徑;不支援時共用同一個路由載入器,透過既有 History API 和框架機制完成導航。不要為了相容性複製兩套業務規則。
相容層要覆蓋首屏、重新整理、前進後退、連結中斷和異常回退。測試矩陣至少包含支援與不支援 API 的瀏覽器、慢網、快速連續導航、跨網域連結、下載連結和腳本載入失敗。能力偵測應與功能使用點接近,避免版本判斷造成錯誤分支。
第六步:驗證效能、可存取性和可觀測性
以真實使用者任務驗證:搜尋、篩選、返回、重新整理、深層連結開啟和錯誤重試。記錄導航開始到可互動的時間、取消率、失敗率、回退率和重複請求;把這些指標按瀏覽器能力分組。不要只測元件渲染耗時,因為導航失敗可能來自資料、權限或歷史狀態。
保留原生連結語意、鍵盤操作和輔助技術可感知的焦點移動。攔截後應更新文件標題,並把焦點放到頁面主內容;不應阻止使用者在新分頁開啟連結。效能最佳化要以可恢復為前提,快取只能減少延遲,不能成為恢復所需的唯一資料來源。
資訊增益與邊界
Navigation API 的價值在於把平台導航事件、歷史記錄和可取消載入放到同一條狀態鏈上。它不能保證資料來源可用,也不能讓跨網域頁面變成同文件路由。面試中應明確:攔截範圍、提交時機、失敗時的舊頁面策略,以及瀏覽器能力不足時的預設行為。
高品質示範回答
「我會先把導航分成同源應用路由、跨文件頁面、下載、表單和跨網域跳轉,只攔截前一類。首屏和普通連結繼續可用,URL 與歷史記錄作為事實來源。支援 Navigation API 時,在 navigate 事件中呼叫 intercept(),為每次導航建立序號和取消訊號;新導航會取消舊載入,提交結果前再次確認它仍是最新任務。
載入成功後更新視圖、標題、焦點和捲動策略。新路由捲到頂部,前進後退恢復歷史位置;需要保存的小狀態用目前歷史項目,避免把大型物件或敏感資訊寫進去。載入失敗時保留舊頁面,顯示可重試、返回或完整重新整理動作,並透過 navigatesuccess、navigateerror 和伺服器日誌記錄耗時、取消和錯誤類型。
如果瀏覽器不支援 Navigation API,就讓同一載入器走 History API 或框架路由;外部連結和下載始終使用預設瀏覽器行為。最後用深層連結、重新整理、連續點擊、慢網、跨網域連結、腳本失敗和輔助技術任務驗證,確認位址、內容、焦點和歷史記錄不會互相失步。這樣得到的是漸進增強的導航層,而不是只在新瀏覽器裡工作的客戶端替換。」
常見錯誤
- 攔截所有連結 → 下載、跨網域和表單語意會被破壞 → 只攔截可確認的同源應用路由。
- 讓最後回傳的請求獲勝 → 舊請求可能覆蓋使用者最新意圖 → 使用取消訊號和導航序號。
- 只測元件渲染 → 資料、權限和歷史狀態錯誤被遺漏 → 覆蓋完整導航任務和失敗回退。
- 把 Navigation API 當成首屏方案 → 重新整理或腳本失敗時頁面無法恢復 → 保留伺服器回應和普通連結。
- 把狀態全部放進歷史項目 → 資料過大或洩露敏感資訊 → 只保存可重建的小狀態。
- 相容層複製一套業務邏輯 → 兩條路徑會逐漸產生不同錯誤 → 共用載入器、狀態機和監控。
追問及應對
使用者連續點擊兩個目標,舊請求已經寫入快取怎麼辦?
快取寫入可以允許,但介面提交必須檢查取消訊號和最新序號。快取項目帶上 URL、參數和版本,後續請求只能讀取與目前目標匹配的資料;不能讓過期結果直接改變頁面。
目標頁面權限失效,應該回退還是跳登入?
先依據伺服器明確的狀態碼區分未登入、無權限和資源不存在。未登入可轉到登入流程並保存返回 URL;無權限保留目前頁面的解釋和返回動作。不要把權限錯誤偽裝成普通網路失敗。
如何測試瀏覽器不支援 Navigation API 的路徑?
在能力偵測分支下執行同一組深層連結、重新整理、前進後退、慢網和錯誤用例,並檢查位址、內容、標題、焦點和捲動。測試目標是行為一致,不是要求兩個實作的內部事件完全相同。
為什麼不直接完全依賴框架路由?
框架路由可以重用,但題目要考察瀏覽器導航邊界。需要明確哪些導航必須由瀏覽器處理、哪些可以增強,以及框架事件如何映射到取消、歷史和錯誤語意。回答應以平台行為為基線,再說明框架適配。