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

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


咨詢
建站咨詢
