close

路由守衛

相信大家對路由守衛都不陌生,其實就是在頁面當前發生導航變化時,在導航變化的前中後時機去做一些其他具體的事情。

SPA & History API

而在前端常見的業務場景,單頁應用即 SPA 中,路由守衛功能則顯得至為重要。目前主流的在 SPA 中實現路由守衛功能的方法,則是藉助 History API 來實現的。基本原理是藉助 window.history.pushState 以及 window.history.replaceState 來隨時改變頁面地址導航,再藉助 window.onpopstate 或者 window.onhashchange 來監聽頁面的導航地址變化。

無法感知的 pushState & replaceState

然而,History API 也不是完全的銀彈,主要在於導航地址監聽器,只能監聽到頁面的前進後退,無法監聽到 pushState 跟 replaceState。但是,一般頁面上都會存在一些交互,會需要隨時調用 pushState 或者 replaceState 來改變頁面導航,與此同時,也需要相應地觸發頁面內的相關部分渲染更新。

為了解決這個無法感知的問題,通常有以下兩種解決方案。

解決方案一

先註冊自定義的 listeners,再給 push 跟 replace 再包一層,封裝出來另外獨立的 push 跟 replace 方法,之後每次調用的都是封裝之後的方法,該方法內部會先真正執行 pushState 以及 replaceState 方法,之後再通知前面註冊的 listeners 去執行。

使用這種解決方案的典型例子是 react-router,具體流程如下圖

但是這種方案其實也是有局限性的,因為他依賴於其他模塊都向同一個地方註冊 listeners 並要求其他模塊都去使用它自定義的封裝過後的 push 跟 replace,其並沒有提供一種通用規範的中心化的解決方案。假如現在頁面中引入了另一部分會更新頁面地址導航的邏輯,但是其並不使用前者封裝的 push 或者 replace 的話,那麼還是沒辦法觸發頁面渲染更新。

不過,接下來介紹的方案二,卻可以解決上述問題。

解決方案二

通過直接暴力重寫 window.history.pushState 跟 window.history.replaceState 方法,提供通用中心化解決方案。類似如下

constrewrite=function(type){consthapi=history[type];returnfunction(){//可以在此處自定義更多的其他邏輯//...constres=hapi.apply(this,arguments);//...//自定義拋出一個popstate事件,讓其他部分監聽popstate事件的代碼,也能感知到consteventArguments=createPopStateEvent(window.history.state,type);window.dispatchEvent(eventArguments);returnres;}};history.pushState=rewrite("pushState");history.replaceState=rewrite("replaceState");

使用這種解決方案的典型例子是 Garfish,其關鍵實現代碼如下

但是這種解決方案,也是有副作用的,畢竟暴力重寫了全局方法,同時還自定義拋出了一個 popstate 事件。

試想一下,假如當前頁面除了有 Garfish 之外,還有另外一個模塊,該模塊自己內部定義了一個經過封裝的 push 方法,其每次調用該 push 方法時,會先調用經過 rewrite 的 window.history.pushState 並觸發一次 popstate 事件,之後又會再通知模塊內部的 listener 執行,與此同時,該模塊內部也監聽到了 popstate 事件並再一次執行了一次 listener,這時候,我們就會發現重複執行了兩次 listener,這便是一個典型的副作用。

這麼看下來,不管是用方案一還是用方案二,其實都或多或少有一些問題,那麼,有沒有其他更好的更通用的中心化的解決方案呢?在 MDN 文檔里查了一圈,都沒有發現比較好的方案,直到在 Chrome 里發現了 window.navigation。

Navigation API 橫空出世

我們先看下 Chrome 的開發者文檔里關於 Navigation API 的簡介,如下

可以看到對於 Navigation API 定位是現代前端原生路由。同時也重點聲明了可以用 Navigation API 重新構建 SPA 的。

NavigateEvent

Navigation API 里比較核心重要的部分,就是 navigate event 了。使用示例如下:

navigation.addEventListener('navigate',navigateEvent=>{switch(navigateEvent.destination.url){case'https://example.com/':navigateEvent.transitionWhile(loadIndexPage());break;case'https://example.com/cats':navigateEvent.transitionWhile(loadCatsPage());break;}});為什麼需要增加一個 NavigateEvent

我們過往在結合 History API 實現 SPA 的時候,為了能感知到 pushState 以及 replaceState,我們是需要通過做很多其他的工作才能做到的,但是,有了 navigate event 之後,我們就可以輕輕鬆鬆通過添加一個事件監聽器,就能監聽到絕大部分的地址導航變化。現在我們再一次執行 pushState 以及 replaceState 的時候,是可以被 navigate 事件的監聽器監聽並感知到的。總的來說,是一種原生的更加通用且中心化的方式。

Transition

Transition 顧名思義,就是可以在頁面發生 navigate event 時,做一些自定義的過渡的操作。其中主要是使用 transitionWhile(),他接受一個 Promise 類型參數,使用方式是,在 navigate 事件監聽器內執行,他的執行,代表着告訴瀏覽器目前正在準備新的狀態新的頁面,這是需要耗費一定時間的,至於具體耗費多長時間,取決於傳入的 Promise 何時 resolved 或者 rejected。

navigation.addEventListener('navigate',navigateEvent=>{if(isCatsUrl(navigateEvent.destination.url)){constprocessNavigation=async()=>{constrequest=awaitfetch('/cat-memes.json',);constjson=awaitrequest.json();//TODO:dosomethingwithcatmemesjson};navigateEvent.transitionWhile(processNavigation());}else{//loadsomeotherpage}});Transition Success and Failure

前面已經提到傳入 transitionWhile() 的 Promise 參數,是有可能成功 resolved 也有可能失敗 rejected 的,而這兩種狀態,分別對應着 Transition Success 以及 Transition Failure,繼而也對應着 navigatesuccess 以及 navigateerror 兩個事件。

當 Promise 達到 fulfills 時,或者是壓根就沒有調用 transitionWhile(),那麼 Navigation API 將會觸發一個 navigatesuccess 事件。

navigation.addEventListener('navigatesuccess',event=>{loadingIndicator.hidden=true;});

當 Promise rejects 時,Navigation API 則會觸發一個 navigateerror 事件。

navigation.addEventListener('navigateerror',event=>{loadingIndicator.hidden=true;//alsohideindicatorshowMessage(`Failedtoloadpage:${event.message}`);});導航取消 Abort Signals

假如當前頁面還正在導航跳轉時,突然被強占了,比如用戶這時突然又點擊了另外一個鏈接進行訪問或者代碼里直接執行了另外一個導航,為了應對這種情況,我們在傳送給 navigate 的事件監聽器的 event 參數對象里,多增加了一個 property 即 signal,類型為 window.AbortSignal。可以結合 AbortSignal 及 fetch 來實現 Abortable fetch,方法是,將 AbortSignal 傳給 fetch,如果當前導航跳轉被搶占了,則可以立即取消掉相應的網絡請求,這樣既可以節省用戶的帶寬,又可以將 fetch 返回的 Promise 置為 rejected 的狀態,以防止任何無效的代碼更新頁面導致出現無效非法的導航頁面。

navigation.addEventListener('navigate',navigateEvent=>{if(isCatsUrl(navigateEvent.destination.url)){constprocessNavigation=async()=>{constrequest=awaitfetch('/cat-memes.json',{signal:navigateEvent.signal,});constjson=awaitrequest.json();//TODO:dosomethingwithcatmemesjson};navigateEvent.transitionWhile(processNavigation());}else{//loadsomeotherpage}}); Entries

Navigation API 也有 Entries 概念,代表的是導航頁面入口。可以通過 navigation.currentEntry 獲取到當前用戶所在的導航頁面入口,也可以通過 navigation.entries() 獲取到用戶導航訪問過的所有入口的列表。其中,Entry 在 Web IDL 中的規範定義如下

interfaceNavigationHistoryEntry:EventTarget{readonlyattributeUSVString?url;readonlyattributeDOMStringkey;readonlyattributeDOMStringid;readonlyattributelonglongindex;readonlyattributebooleansameDocument;anygetState();attributeEventHandlerondispose;};
url:導航會話的 URL 地址
key:在導航會話歷史棧中的唯一標識,id 與 key 的區別在於,key 標識是在棧中的唯一標識,id 是 NavigationHistoryEntry 實例的唯一標識。例如:調用 replace 或 reload 時並沒有產生新的導航會話,但會生成新的 NavigationHistoryEntry,前後兩個 NavigationHistoryEntry 實例的 key 相同,但 id 不同。
id:導航會話的唯一標識
index:指示該導航會話在歷史棧的位置,默認從 0 開始
sameDocument:true 代表當前是處於激活狀態,false 則表示未激活
getState:返回導航會話存儲的狀態,類似 history.state
ondispose:監聽 dispose 事件,在該導航會話從歷史棧中刪除時觸發

可以通過 getState() 來獲取 Entries 的 State,例如 navigation.currentEntry.getState(),這裡的 State 也可以通過 navigation.updateCurrentEntry({state: something}); 來更新。

導航操作
navigation.navigate(url: string, options:state: any, history: 'auto' | 'push' | 'replace')

打開目標地址頁面,相等於 history.pushState 和 history.replaceState,但是支持跨域地址。

navigation.reload({ state: any })

刷新當前頁面,相當於調用了 location.reload()

navigation.back()

在導航會話歷史中向後移動一頁,相當於 history.back()

navigation.forward()

在導航會話歷史中向前移動一頁,相當於 history.forward()

navigation.traverseTo(key: string)

在導航會話歷史記錄中加載特定頁面,相當於 history.go(),但區別在於傳參不同,navigation 給每個導航會話設置了一個唯一標識,traverseTo 接受的參數正是該唯一標識,即 NavigationHistoryEntry.key。

不足

新 API,兼容性不好

實際上,Navigation API 是從 Chrome 102 才開始支持的,查了下筆者的 Chrome 版本,也才剛到 103 版本。。。。。。

展望

本文所闡述的內容,核心並不是為了詳盡詳細地介紹 Navigation 各個 API 的使用細節,而是想表達一下目前用 History API 來實現 SPA 所涉及的問題,並延伸介紹一下實現 SPA 的更好解決方案。個人認為,Navigation API 將有可能是未來的趨勢,或許在不久的將來,他將是實現 SPA 的主要方案,而 History API 則可能更多成為一種 fallback 方案。

參考文獻
https://developer.chrome.com/docs/web-platform/navigation-api
https://wicg.github.io/navigation-api/

-END-

關於奇舞團

奇舞團是 360 集團最大的大前端團隊,代表集團參與 W3C 和 ECMA 會員(TC39)工作。奇舞團非常重視人才培養,有工程師、講師、翻譯官、業務接口人、團隊 Leader 等多種發展方向供員工選擇,並輔以提供相應的技術力、專業力、通用力、領導力等培訓課程。奇舞團以開放和求賢的心態歡迎各種優秀人才關注和加入奇舞團。

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()