新聞中心
路由守衛(wèi)
相信大家對路由守衛(wèi)都不陌生,其實就是在頁面當(dāng)前發(fā)生導(dǎo)航變化時,在導(dǎo)航變化的前中后時機去做一些其他具體的事情。

10年積累的做網(wǎng)站、成都網(wǎng)站制作經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先網(wǎng)站設(shè)計后付款的網(wǎng)站建設(shè)流程,更有阜新免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
SPA & History API
而在前端常見的業(yè)務(wù)場景,單頁應(yīng)用即 SPA 中,路由守衛(wèi)功能則顯得至為重要。目前主流的在 SPA 中實現(xiàn)路由守衛(wèi)功能的方法,則是借助 History API 來實現(xiàn)的?;驹硎墙柚?window.history.pushState 以及 window.history.replaceState 來隨時改變頁面地址導(dǎo)航,再借助 window.onpopstate 或者 window.onhashchange 來監(jiān)聽頁面的導(dǎo)航地址變化。
無法感知的 pushState & replaceState
然而,History API 也不是完全的銀彈,主要在于導(dǎo)航地址監(jiān)聽器,只能監(jiān)聽到頁面的前進后退,無法監(jiān)聽到 pushState 跟 replaceState。但是,一般頁面上都會存在一些交互,會需要隨時調(diào)用 pushState 或者 replaceState 來改變頁面導(dǎo)航,與此同時,也需要相應(yīng)地觸發(fā)頁面內(nèi)的相關(guān)部分渲染更新。
為了解決這個無法感知的問題,通常有以下兩種解決方案。
?解決方案一?
先注冊自定義的 listeners,再給 push 跟 replace 再包一層,封裝出來另外獨立的 push 跟 replace 方法,之后每次調(diào)用的都是封裝之后的方法,該方法內(nèi)部會先真正執(zhí)行 pushState 以及 replaceState 方法,之后再通知前面注冊的 listeners 去執(zhí)行。
使用這種解決方案的典型例子是 react-router,具體流程如下圖
但是這種方案其實也是有局限性的,因為他依賴于其他模塊都向同一個地方注冊 listeners 并要求其他模塊都去使用它自定義的封裝過后的 push 跟 replace,其并沒有提供一種通用規(guī)范的中心化的解決方案。假如現(xiàn)在頁面中引入了另一部分會更新頁面地址導(dǎo)航的邏輯,但是其并不使用前者封裝的 push 或者 replace 的話,那么還是沒辦法觸發(fā)頁面渲染更新。
不過,接下來介紹的方案二,卻可以解決上述問題。
?解決方案二?
通過直接暴力重寫 window.history.pushState 跟 window.history.replaceState 方法,提供通用中心化解決方案。類似如下
const rewrite = function(type) {
const hapi = history[type];
return function() {
// 可以在此處自定義更多的其他邏輯
// ...
const res = hapi.apply(this, arguments);
// ...
// 自定義拋出一個 popstate 事件,讓其他部分監(jiān)聽 popstate 事件的代碼,也能感知到
const eventArguments = createPopStateEvent(window.history.state, type);
window.dispatchEvent(eventArguments);
return res;
}
};
history.pushState = rewrite("pushState");
history.replaceState = rewrite("replaceState");使用這種解決方案的典型例子是 Garfish,其關(guān)鍵實現(xiàn)代碼如下
但是這種解決方案,也是有副作用的,畢竟暴力重寫了全局方法,同時還自定義拋出了一個 popstate 事件。
試想一下,假如當(dāng)前頁面除了有 Garfish 之外,還有另外一個模塊,該模塊自己內(nèi)部定義了一個經(jīng)過封裝的 push 方法,其每次調(diào)用該 push 方法時,會先調(diào)用經(jīng)過 rewrite 的 window.history.pushState 并觸發(fā)一次 popstate 事件,之后又會再通知模塊內(nèi)部的 listener 執(zhí)行,與此同時,該模塊內(nèi)部也監(jiān)聽到了 popstate 事件并再一次執(zhí)行了一次 listener,這時候,我們就會發(fā)現(xiàn)重復(fù)執(zhí)行了兩次 listener,這便是一個典型的副作用。
這么看下來,不管是用方案一還是用方案二,其實都或多或少有一些問題,那么,有沒有其他更好的更通用的中心化的解決方案呢?在 MDN 文檔里查了一圈,都沒有發(fā)現(xiàn)比較好的方案,直到在 Chrome 里發(fā)現(xiàn)了 window.navigation。
Navigation API 橫空出世
我們先看下 Chrome 的開發(fā)者文檔里關(guān)于 Navigation API 的簡介,如下
可以看到對于 Navigation API 定位是現(xiàn)代前端原生路由。同時也重點聲明了可以用 Navigation API 重新構(gòu)建 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
我們過往在結(jié)合 History API 實現(xiàn) SPA 的時候,為了能感知到 pushState 以及 replaceState,我們是需要通過做很多其他的工作才能做到的,但是,有了 navigate event 之后,我們就可以輕輕松松通過添加一個事件監(jiān)聽器,就能監(jiān)聽到絕大部分的地址導(dǎo)航變化。現(xiàn)在我們再一次執(zhí)行 pushState 以及 replaceState 的時候,是可以被 navigate 事件的監(jiān)聽器監(jiān)聽并感知到的??偟膩碚f,是一種原生的更加通用且中心化的方式。
?Transition?
Transition 顧名思義,就是可以在頁面發(fā)生 navigate event 時,做一些自定義的過渡的操作。其中主要是使用 transitionWhile(),他接受一個 Promise 類型參數(shù),使用方式是,在 navigate 事件監(jiān)聽器內(nèi)執(zhí)行,他的執(zhí)行,代表著告訴瀏覽器目前正在準(zhǔn)備新的狀態(tài)新的頁面,這是需要耗費一定時間的,至于具體耗費多長時間,取決于傳入的 Promise 何時 resolved 或者 rejected。
navigation.addEventListener('navigate', navigateEvent => {
if (isCatsUrl(navigateEvent.destination.url)) {
const processNavigation = async () => {
const request = await fetch('/cat-memes.json',);
const json = await request.json();
// TODO: do something with cat memes json
};
navigateEvent.transitionWhile(processNavigation());
} else {
// load some other page
}
});
Transition Success and Failure
前面已經(jīng)提到傳入 transitionWhile() 的 Promise 參數(shù),是有可能成功 resolved 也有可能失敗 rejected 的,而這兩種狀態(tài),分別對應(yīng)著 Transition Success 以及 Transition Failure,繼而也對應(yīng)著 navigatesuccess 以及 navigateerror 兩個事件。
當(dāng) Promise 達到 fulfills 時,或者是壓根就沒有調(diào)用 transitionWhile(),那么 Navigation API 將會觸發(fā)一個 navigatesuccess 事件。
navigation.addEventListener('navigatesuccess', event => {
loadingIndicator.hidden = true;
});當(dāng) Promise rejects 時,Navigation API 則會觸發(fā)一個 navigateerror 事件。
navigation.addEventListener('navigateerror', event => {
loadingIndicator.hidden = true; // also hide indicator
showMessage(`Failed to load page: ${event.message}`);
});
導(dǎo)航取消 Abort Signals
假如當(dāng)前頁面還正在導(dǎo)航跳轉(zhuǎn)時,突然被強占了,比如用戶這時突然又點擊了另外一個鏈接進行訪問或者代碼里直接執(zhí)行了另外一個導(dǎo)航,為了應(yīng)對這種情況,我們在傳送給 navigate 的事件監(jiān)聽器的 event 參數(shù)對象里,多增加了一個 property 即 signal,類型為 window.AbortSignal。可以結(jié)合 AbortSignal 及 fetch 來實現(xiàn) Abortable fetch,方法是,將 AbortSignal 傳給 fetch,如果當(dāng)前導(dǎo)航跳轉(zhuǎn)被搶占了,則可以立即取消掉相應(yīng)的網(wǎng)絡(luò)請求,這樣既可以節(jié)省用戶的帶寬,又可以將 fetch 返回的 Promise 置為 rejected 的狀態(tài),以防止任何無效的代碼更新頁面導(dǎo)致出現(xiàn)無效非法的導(dǎo)航頁面。
navigation.addEventListener('navigate', navigateEvent => {
if (isCatsUrl(navigateEvent.destination.url)) {
const processNavigation = async () => {
const request = await fetch('/cat-memes.json', {
signal: navigateEvent.signal,
});
const json = await request.json();
// TODO: do something with cat memes json
};
navigateEvent.transitionWhile(processNavigation());
} else {
// load some other page
}
});
?Entries?
Navigation API 也有 Entries 概念,代表的是導(dǎo)航頁面入口。可以通過 navigation.currentEntry 獲取到當(dāng)前用戶所在的導(dǎo)航頁面入口,也可以通過 navigation.entries() 獲取到用戶導(dǎo)航訪問過的所有入口的列表。其中,Entry 在 Web IDL 中的規(guī)范定義如下
interface NavigationHistoryEntry : EventTarget {
readonly attribute USVString? url;
readonly attribute DOMString key;
readonly attribute DOMString id;
readonly attribute long long index;
readonly attribute boolean sameDocument;
any getState();
attribute EventHandler ondispose;
};
- url:導(dǎo)航會話的 URL 地址
- key:在導(dǎo)航會話歷史棧中的唯一標(biāo)識,id 與 key 的區(qū)別在于,key 標(biāo)識是在棧中的唯一標(biāo)識,id 是 NavigationHistoryEntry 實例的唯一標(biāo)識。例如:調(diào)用 replace 或 reload 時并沒有產(chǎn)生新的導(dǎo)航會話,但會生成新的 NavigationHistoryEntry,前后兩個 NavigationHistoryEntry 實例的 key 相同,但 id 不同。
- id:導(dǎo)航會話的唯一標(biāo)識
- index:指示該導(dǎo)航會話在歷史棧的位置,默認(rèn)從 0 開始
- sameDocument:true 代表當(dāng)前是處于激活狀態(tài),false 則表示未激活
- getState:返回導(dǎo)航會話存儲的狀態(tài),類似 history.state
- ondispose:監(jiān)聽 dispose 事件,在該導(dǎo)航會話從歷史棧中刪除時觸發(fā)
可以通過 getState() 來獲取 Entries 的 State,例如 navigation.currentEntry.getState(),這里的 State 也可以通過 navigation.updateCurrentEntry({state: something}); 來更新。
?導(dǎo)航操作?
- navigation.navigate(url: string, options:state: any, history: 'auto' | 'push' | 'replace')
打開目標(biāo)地址頁面,相等于 history.pushState 和 history.replaceState,但是支持跨域地址。
- navigation.reload({ state: any })
刷新當(dāng)前頁面,相當(dāng)于調(diào)用了 location.reload()
- navigation.back()
在導(dǎo)航會話歷史中向后移動一頁,相當(dāng)于 history.back()
- navigation.forward()
在導(dǎo)航會話歷史中向前移動一頁,相當(dāng)于 history.forward()
- navigation.traverseTo(key: string)
在導(dǎo)航會話歷史記錄中加載特定頁面,相當(dāng)于 history.go(),但區(qū)別在于傳參不同,navigation 給每個導(dǎo)航會話設(shè)置了一個唯一標(biāo)識,traverseTo 接受的參數(shù)正是該唯一標(biāo)識,即 NavigationHistoryEntry.key。
?不足?
新 API,兼容性不好
實際上,Navigation API 是從 Chrome 102 才開始支持的,查了下筆者的 Chrome 版本,也才剛到 103 版本。
展望
本文所闡述的內(nèi)容,核心并不是為了詳盡詳細地介紹 Navigation 各個 API 的使用細節(jié),而是想表達一下目前用 History API 來實現(xiàn) SPA 所涉及的問題,并延伸介紹一下實現(xiàn) SPA 的更好解決方案。個人認(rèn)為,Navigation API 將有可能是未來的趨勢,或許在不久的將來,他將是實現(xiàn) SPA 的主要方案,而 History API 則可能更多成為一種 fallback 方案。
參考文獻
- https://developer.chrome.com/docs/web-platform/navigation-api
- https://wicg.github.io/navigation-api/
aPaaS Growth 團隊專注在用戶可感知的、宏觀的 aPaaS 應(yīng)用的搭建流程,及租戶、應(yīng)用治理等產(chǎn)品路徑,致力于打造 aPaaS 平臺流暢的 “應(yīng)用交付” 流程和體驗,完善應(yīng)用構(gòu)建相關(guān)的生態(tài),加強應(yīng)用搭建的便捷性和可靠性,提升應(yīng)用的整體性能,從而助力 aPaaS 的用戶增長,與基礎(chǔ)團隊一起推進 aPaaS 在企業(yè)內(nèi)外部的落地與提效。
本文名稱:MDN里暫時還查不到的NavigationAPI
轉(zhuǎn)載注明:http://www.5511xx.com/article/djpedhd.html


咨詢
建站咨詢
