当前位置: 首页 > 科技观察

MDN上暂时没有的NavigationAPI

时间:2023-03-11 23:46:53 科技观察

路由守卫,相信大家对路由守卫并不陌生。其实就是在页面当前导航发生变化的时候,在导航变化之前、之中、之后做一些其他特定的事情。SPA&HistoryAPI在常见的前端业务场景,单页应用(SPA)中,路由守卫功能非常重要。目前在SPA中实现路由守卫功能的主流方法是借助HistoryAPI来实现。基本原理是利用window.history.pushState和window.history.replaceState随时改变页面地址导航,然后利用window.onpopstate或window.onhashchange监听页面导航地址变化。不了解pushState&replaceState然而HistoryAPI并不是一个完整的灵丹妙药,主要是导航地址监听器,它只能监听页面的前进和后退,无法监听pushState和replaceState。但是一般页面会有一些交互,需要随时调用pushState或者replaceState来改变页面导航。同时,你还需要相应地触发页面相关部分的渲染更新。为了解决这个不易察觉的问题,通常有以下两种解决方案。方案一先注册自定义监听器,然后再包裹一层推送替换,再封装一个独立的推送替换方法,然后每次调用封装好的方法。执行pushState和replaceState方法,然后通知之前注册的监听器执行。使用这种方案的一个典型例子是react-router,具体流程如下图所示,但这种方案实际上是有局限性的,因为它依赖于其他模块将监听器注册到同一个地方,需要其他模块使用它自己定义的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);返回资源;}};history.pushState=rewrite("pushState");history.replaceState=rewrite("replaceState");使用这个方案的一个典型例子是Garfish,它的关键实现代码如下但是这个方案也有副作用。毕竟暴力重写了全局方法,还自定义了一个popstate事件。试想一下,如果当前页面除了Garfish之外还有另外一个模块,模块本身定义了一个封装的push方法,每次调用push方法都会先调用重写的window.history。pushState并触发一个popstate事件,然后通知模块内部的监听器执行。同时模块也监听popstate事件,再次执行监听器。这时候我们会发现它执行了两次listener,这是典型的副作用。这样看,不管是用方案1还是方案2,其实或多或少都存在问题。那么,有没有其他更好更通用的中心化方案呢?在MDN文档中查找后,一直没有找到更好的解决方案,直到在Chrome中找到了window.navigation。NavigationAPI的诞生我们先来看看Chrome的开发者文档中对NavigationAPI的介绍。如下图所示,NavigationAPI定位为现代前端原生路由。同时,也凸显了SPA可以通过NavigationAPI进行重构。NavigateEventNavigationAPI的核心和重要部分是导航事件。示例用法如下: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当我们以前结合??HistoryAPI实现SPA时,为了知道pushState和replaceState,我们需要做很多其他的工作来做到这一点,但是有了navigate事件,我们可以通过添加事件监听器轻松监听大部分地址导航变化。现在我们再次执行pushState和replaceState时,就可以被navigate事件的监听器监听和感知了。总的来说,它是一种原生的、更通用的、中心化的方式。TransitionTransition,顾名思义,就是在页面发生navigate事件时,做一些自定义的transition操作。最主要的是使用transitionWhile(),它接受一个Promise类型的参数。它用于导航事件侦听器。它的执行意味着告诉浏览器它当前正在准备一个具有新状态的新页面,这需要很多时间。这需要一定的时间。至于需要多长时间,要看传入的Promise什么时候被resolved或者rejected。navigation.addEventListener('navigate',navigateEvent=>{if(isCatsUrl(navigateEvent.destination.url)){constprocessNavigation=async()=>{constrequest=awaitfetch('/cat-memes.json',);constjson=awaitrequest.json();//TODO:用catmemesjson做点什么};navigateEvent.transitionWhile(processNavigation());}else{//加载其他页面}});TransitionSuccess和Failure之前已经提到过传递给transitionWhile()的Promise参数可能resolve成功也可能rejected,这两种状态分别对应TransitionSuccess和TransitionFailure,然后也对应navigatesuccess和navigateerror事件。当Promise完成时,或者根本没有调用transitionWhile()时,NavigationAPI将触发navigatesuccess事件。navigation.addEventListener('navigatesuccess',event=>{loadingIndicator.hidden=true;});当Promise拒绝时,NavigationAPI将触发navigateerror事件。navigation.addEventListener('navigateerror',event=>{loadingIndicator.hidden=true;//也隐藏指示器showMessage(`Failedtoloadpage:${event.message}`);});Navigation取消AbortSignals如果当前页面还在导航跳转的时候,突然被占用了。比如用户突然点击另一个链接访问或者直接执行代码中的另一个导航。为了应对这种情况,我们在发送给navigate的事件监听器的事件参数对象中,多了一个属性signal,类型为window.AbortSignal。可以通过结合AbortSignal和fetch来实现Abortablefetch。方法是通过AbortSignal来fetch。如果当前导航跳转被抢占,可以立即取消相应的网络请求,这样不仅可以节省用户的带宽,还可以将fetch返回的Promise设置为拒绝状态,防止任何无效代码更新页面导致无效和非法的导航页面。navigation.addEventListener('navigate',navigateEvent=>{if(isCatsUrl(navigateEvent.destination.url)){constprocessNavigation=async()=>{constrequest=awaitfetch('/cat-memes.json',{信号:navigateEvent.signal,});constjson=awaitrequest.json();//TODO:用catmemesjson做点什么};navigateEvent.transitionWhile(processNavigation());}else{//加载其他页面}});EntriesNavigationAPI也有Entries的概念,代表导航页面的入口。可以通过navigation.currentEntry获取当前用户的导航页面入口,也可以通过navigation.entries()获取用户浏览过的所有入口的列表。其中,Entry在WebIDL中的规范定义如下interfaceNavigationHistoryEntry:EventTarget{readonlyattributeUSVString?网址;只读属性DOMString键;只读属性DOMStringid;readonly属性longlong索引;只读属性布尔值sameDocument;ondispose;};url:导航会话的URL地址key:导航会话历史栈中的唯一标识,id和key的区别是key标识是栈中的唯一标识,id是唯一的NavigationHistoryEntry实例的标识符。例如:调用replace或reload时,不会产生新的navigationsession,但会产生新的NavigationHistoryEntry。前后两个NavigationHistoryEntry实例的key相同,只是id不同。id:导航会话的唯一标识index:表示导航会话在历史栈中的位置,默认从0开始sameDocument:true表示当前处于活动状态,false表示不处于活动状态导航会话,类似于历史。stateondispose:监听dispose事件,当导航会话从历史栈中删除时触发。Entries的State可以通过getState()获取,比如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为每个navigationsession设置了一个唯一标识,traverseTo接受的参数正是唯一标识,即NavigationHistoryEntry.key。新API不足,兼容性差其实NavigationAPI是从Chrome102开始才支持的,我查了一下作者的Chrome版本,刚到103版本。。....期待本文描述的内容,核心不是详细介绍Navigation各个API的使用细节,而是表达目前使用HistoryAPI实现SPA所涉及的问题,并延伸介绍实施SPA的更好解决方案。个人认为NavigationAPI可能是未来的趋势。也许在不久的将来,它将成为实现SPA的主要解决方案,而HistoryAPI可能更多地成为一种后备解决方案。参考资料https://developer.chrome.com/docs/web-platform/navigation-apihttps://wicg.github.io/navigation-api/aPaaS增长团队专注于构建用户可感知和宏观的aPaaS应用流程、租户、应用治理等产品路径致力于在aPaaS平台打造流畅的“应用交付”流程和体验,完善应用建设相关生态,增强应用建设的便捷性和可靠性,提升应用的整体性能。助力aPaaS用户的成长,与基础团队共同推动aPaaS在企业内外的落地和效率。