全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-09-29_vue router 4 源码篇:router history的原生结合

您的位置:首页 >> 新闻 >> 行业资讯

vue router 4 源码篇:router history的原生结合 本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! 源码专栏感谢大家继续阅读《Vue Router 4 源码探索系列》专栏,你可以在下面找到往期文章: 《vue router 4 源码篇:路由诞生——createRouter原理探索》《vue router 4 源码篇:路由matcher的前世今生》《vue router 4 源码篇:router history的原生结合》开场哈喽大咖好,我是跑手,本次给大家继续探讨vue-router@4.x源码中有关Web History API能力的部分,也就是官方文档中历史模式。 大家多少有点了解,包括react router、vue-router在内大多数单页路由库,是基于H5 History API能力来实现的。History API其实做的事情也很简单,就是改变当前web URL而不与服务器交互,完成纯前端页面的URL变型。 撰写目的在这篇文章里,你能获得以下增益: 了解vue-router中对Web History API能力的应用。了解createWebHistory和createWebHashHistory的实现原理。事不宜迟,开讲。。。 。。 。 Web History API在H5 History API完成页面url变化有2个重要函数:pushState()和replaceState(),它们的差异无非就是 举个沉浸式例子我们随便打开一个页面,在控制台查看下原始History是这样的,其中length是一个只读属性,代表当前 session记录的页面历史数量(包括当前页)。 image.png然后再执行这段代码,有得到如下效果:浏览器url发生了变化,但页面内容没有任何改动: history.pushState( {myName:'test',state:{page:1,index:2}}, 'divtitle', '/divPath' ) 我们再看看History内容,如下图: image.png会发现和之前的变化有: length由2变3。虽然页面不跳转,但我们执行pushState时往history堆栈中插入了一条新数据,所以依旧被History对象收录,因此length加1;scrollRestoration是描述页面滚动属性,auto|manual: 分别表示自动 | 手动恢复页面滚动位置,在vue-router滚动行为中就用到这块的能力;History.state值变成了我们在pushState传的第一个参数,理论上这个参数可以是任意对象,这也是单页应用在路由跳转时可以随心所欲传值的关键。另外如果不是pushState()和replaceState()调用,state 的值将会是 null。服务器适配用pushState()和replaceState()改变URL确实也有个通病,就是刷新页面报404,因为刷新行为属于浏览器与后台服务通信的默认行为,服务器没法解析前端自定义path而导致404错误。 image.png要解决这个问题,你需要在服务器上添加一个简单的回退路由,如果 URL 不匹配任何静态资源,直接回退到 index.html。 结论说了那么多,总结下Web History API能给我们带来: 在不与服务端交互情况下改变页面url,给单页路由应用带来可玩(有)性(戏)能传值,并且能在history栈顶的state读到这些值,解决单页之间的跳转数据传输问题兼容性好,主流和不是那么主流的客户端都兼容基于此,各类的路由库应用应运而生,当然vue-router也是其中之一。 createWebHistory创建一个适配Vue的 H5 History记录,需要用到createWebHistory方法,入参是一个路径字符串,表示history的根路径,返回是一个vue的history对象,返回类型定义如下: Typescript类型: exportdeclarefunctioncreateWebHistory(base?:string):RouterHistory /** *InterfaceimplementedbyHistoryimplementationsthatcanbepassedtothe *routeras{@linkRouter.history} * *@alpha */ exportinterfaceRouterHistory{ /** *Basepaththatisprependedtoeveryurl.ThisallowshostinganSPAata *sub-folderofadomainlike`example.com/sub-folder`byhavinga`base`of *`/sub-folder` */ readonlybase:string /** *CurrentHistorylocation */ readonlylocation:HistoryLocation /** *CurrentHistorystate */ readonlystate:HistoryState //readonlylocation:ValueContainerHistoryLocationNormalized /** *Navigatestoalocation.InthecaseofanHTML5Historyimplementation, *thiswillcall`history.pushState`toeffectivelychangetheURL. * *@paramto-locationtopush *@paramdata-optional{@linkHistoryState}tobeassociatedwiththe *navigationentry */ push(to:HistoryLocation,data?:HistoryState):void /** *Sameas{@linkRouterHistory.push}butperformsa`history.replaceState` *insteadof`history.pushState` * *@paramto-locationtoset *@paramdata-optional{@linkHistoryState}tobeassociatedwiththe *navigationentry */ replace(to:HistoryLocation,data?:HistoryState):void /** *Traverseshistoryinagivendirection. * *@example *```js *myHistory.go(-1)//equivalenttowindow.history.back() *myHistory.go(1)//equivalenttowindow.history.forward() *``` * *@paramdelta-distancetotravel.Ifdeltais0,itwillgoback, *ifit's0,itwillgoforwardbythatamountofentries. *@paramtriggerListeners-whetherthisshouldtriggerlistenersattachedto *thehistory */ go(delta:number,triggerListeners?:boolean):void /** *AttachalistenertotheHistoryimplementationthatistriggeredwhenthe *navigationistriggeredfromoutside(liketheBrowserbackandforward *buttons)orwhenpassing`true`to{@linkRouterHistory.back}and *{@linkRouterHistory.forward} * *@paramcallback-listenertoattach *@returnsacallbacktoremovethelistener */ listen(callback:NavigationCallback):()=void /** *Generatesthecorrespondinghreftobeusedinananchortag. * *@paramlocation-historylocationthatshouldcreateanhref */ createHref(location:HistoryLocation):string /** *Clearsanyeventlistenerattachedbythehistoryimplementation. */ destroy():void } 在《vue router 4 源码篇:路由诞生——createRouter原理探索》中讲到,createRouter创建vue-router实例时,会添加单页跳转时的监听回调,其能力源于本方法createWebHistory创建的history对象。该对象中导出的方法(如:listen、destroy、push等等...),都是依托了原生Web History API能力,并且结合了Vue技术而封装的中间层SDK,把两者连接起来。 实现原理流程图image.pngcreateWebHistory总流程非常简单,分4步走: 创建vue router的history对象,包含4个属性:location(当前location)、state(路由页面的history state)、和push、replace2个方法;创建vue router监听器:主要支持路由跳转时的state处理和自定义的跳转逻辑回调;添加location劫持,当routerHistory.location变动时返回标准化的路径;添加state劫持,当routerHistory.state变动时返回里面的state;步骤对应的源码如下「附注释」: /** *CreatesanHTML5history.Mostcommonhistoryforsinglepageapplications. * *@parambase- */ exportfunctioncreateWebHistory(base?:string):RouterHistory{ base=normalizeBase(base) //步骤1:创建`vuerouter`的history对象 consthistoryNavigation=useHistoryStateNavigation(base) //步骤2:创建`vuerouter`监听器 consthistoryListeners=useHistoryListeners( base, historyNavigation.state, historyNavigation.location, historyNavigation.replace ) functiongo(delta:number,triggerListeners=true){ if(!triggerListeners)historyListeners.pauseListeners() history.go(delta) } //组装routerHistory对象 constrouterHistory:RouterHistory=assign( { //it'soverriddenrightafter location:'', base, go, createHref:createHref.bind(null,base), }, historyNavigation, historyListeners ) //步骤3:添加location劫持 Object.defineProperty(routerHistory,'location',{ enumerable:true, get:()=historyNavigation.location.value, }) //步骤4:添加state劫持 Object.defineProperty(routerHistory,'state',{ enumerable:true, get:()=historyNavigation.state.value, }) //返回整个routerHistory对象 returnrouterHistory } 最后,createWebHistory方法返回处理好后的routerHistory对象,供createRouter使用。 接下来,我们跟着源码,拆分上面四个流程,看具体是怎么实现的。 创建History第一步,创建vue router的history对象,在上面源码用useHistoryStateNavigation方法来创建这个对象,方便大家理解,笔者简化一个流程图: 流程图image.png从左到右,vue router history使用了H5 History能力。其中history.pushState和history.replaceState方法被封装到一个名为locationChange的路径变化处理函数中,而locationChange作为一个公共函数,则被push 和 replace 函数调用,这2个函数,也就是我们熟知的Router push和Router replace方法。 另外,vue router history的state对象底层也是用到了history.state,只不过再封装成符合vue router的state罢了。 最后,useHistoryStateNavigation方法把push、replace、state、location集成到一个对象中返回,完成了history的初始化。 源码解析changeLocation先看changeLocation,源码如下: functionchangeLocation( to:HistoryLocation, state:StateEntry, replace:boolean ):void{ /** *ifabasetagisprovided,andweareonanormaldomain,wehaveto *respecttheprovided`base`attributebecausepushState()willuseitand *potentiallyeraseanythingbeforethe`#`likeat *https://github.com/vuejs/router/issues/685whereabaseof *`/folder/#`butabaseof`/`woulderasethe`/folder/`section.If *thereisnohost,the`base`tagmakesnosenseandifthereisn'ta *basetagwecanjustuseeverythingafterthe`#`. */ consthashIndex=base.indexOf('#') consturl= hashIndex-1 ?(location.hostdocument.querySelector('base') ?base :base.slice(hashIndex))+to :createBaseLocation()+base+to try{ //BROWSERQUIRK //NOTE:SafarithrowsaSecurityErrorwhencallingthisfunction100timesin30seconds history[replace?'replaceState':'pushState'](state,'',url) historyState.value=state }catch(err){ if(__DEV__){ warn('Errorwithpush/replaceState',err) }else{ console.error(err) } //Forcethenavigation,thisalsoresetsthecallcount location[replace?'replace':'assign'](url) } } 首先是结合base根路径计算最终的跳转url,然后根据replace标记决定使用history.pushState或history.replaceState进行跳转。 buildStatereplace和push里都使用到一个公共函数buildState,这函数作用是在原来state中添加页面滚动位置记录,方便页面回退时滚动到原来位置。 /** *Createsastateobject */ functionbuildState( back:HistoryLocation|null, current:HistoryLocation, forward:HistoryLocation|null, replaced:boolean=false, computeScroll:boolean=false ):StateEntry{ return{ back, current, forward, replaced, position:window.history.length, scroll:computeScroll?computeScrollPosition():null, } } //computeScrollPosition方法定义 exportconstcomputeScrollPosition=()= ({ left:window.pageXOffset, top:window.pageYOffset, }as_ScrollPositionNormalized) replacereplace方法实现也比较简单:先把state和传进来的data整合得到一个最终state,再调用changeLocation进行跳转,最后更新下当前Location变量。 functionreplace(to:HistoryLocation,data?:HistoryState){ conststate:StateEntry=assign( {}, history.state, buildState( historyState.value.back, //keepbackandforwardentriesbutoverridecurrentposition to, historyState.value.forward, true ), data, {position:historyState.value.position} ) changeLocation(to,state,true) currentLocation.value=to } pushfunctionpush(to:HistoryLocation,data?:HistoryState){ //Addtocurrententrytheinformationofwherewearegoing //aswellassavingthecurrentposition constcurrentState=assign( {}, //usecurrenthistorystatetogracefullyhandleawrongcallto //history.replaceState //https://github.com/vuejs/router/issues/366 historyState.value, history.stateasPartialStateEntry|null, { forward:to, scroll:computeScrollPosition(), } ) if(__DEV__!history.state){ warn( `history.stateseemstohavebeenmanuallyreplacedwithoutpreservingthenecessaryvalues.Makesuretopreserveexistinghistorystateifyouaremanuallycallinghistory.replaceState:\n\n`+ `history.replaceState(history.state,'',url)\n\n`+ `Youcanfindmoreinformationathttps://next.router.vuejs.org/guide/migration/#usage-of-history-state.` ) } changeLocation(currentState.current,currentState,true) conststate:StateEntry=assign( {}, buildState(currentLocation.value,to,null), {position:currentState.position+1}, data ) changeLocation(to,state,false) currentLocation.value=to } 和replace差不多,都是调用changeLocation完成跳转,但是push方法会跳转2次:第一次是给router history添加forward和scroll的中间跳转,其作用是保存当前页面的滚动位置。 为什么要2次跳转才能保存页面位置?大家试想下,当你浏览一个页面,滚动到某个位置,你利用history.pushState跳转到另一个页面时,history堆栈会压入一条记录,但同时vue router会帮助你记录跳转前页面位置,以便在回退时恢复滚动位置。要实现这个效果,就必须在push方法中,在调用changeLocation前把当前页面位置记录到router state中。 要实现这个功能方法有多种,最简单方法就是在跳转前把位置信息记录好放进state里面,然后通过changeLocation(to, state, false)实现跳转。 但官方用了另一种优雅方法解决这个问题,就是在最终跳转前先来一次replace模式的中间跳转,这样在不破坏原页面信息基础上更新了router state,省去更多与页面位置相关的连带处理。这就有了push方法中2次调用changeLocation。 至此,vue router history的创建流程全部执行完成,但仅仅依靠history的改变是不够的,下面我们再看看监听器的实现过程。 创建路由监听器流程图image.png众所周知,history.go、history.forward、history.back都会触发popstate事件,然后再将popStateHandler方法绑定到popstate事件即可实现路由跳转监听。 而页面关闭或离开时会触发beforeunload事件,同理将beforeUnloadListener方法绑定到该事件上实现对此类场景的监控。 最后为了能自定义监控逻辑,监听器抛出了3个钩子函数:pauseListeners「停止监听」、listen「注册监听回调,符合订阅发布模式」、destroy「卸载监听器」。 源码解析popStateHandlerconstpopStateHandler:PopStateListener=({ state, }:{ state:StateEntry|null })={ //新跳转地址 constto=createCurrentLocation(base,location) //当前路由地址 constfrom:HistoryLocation=currentLocation.value //当前state constfromState:StateEntry=historyState.value //计步器 letdelta=0 if(state){ //目标路由state不为空时,更新currentLocation和historyState缓存 currentLocation.value=to historyState.value=state //暂停监控时,中断跳转并重置pauseState if(pauseStatepauseState===from){ pauseState=null return } //计算距离 delta=fromState?state.position-fromState.position:0 }else{ //否则执行replace回调 replace(to) } //console.log({deltaFromCurrent}) //Herewecouldalsorevertthenavigationbycallinghistory.go(-delta) //thislistenerwillhavetobeadaptedtonottriggeragainandtowaitfortheurl //tobeupdatedbeforetriggeringthelisteners.Somekindofvalidationfunctionwouldalso //needtobepassedtothelistenerssothenavigationcanbeaccepted //callalllisteners //发布跳转事件,将Location、跳转类型、跳转距离等信息返回给所有注册的订阅者,并执行注册回调 listeners.forEach(listener={ listener(currentLocation.value,from,{ delta, type:NavigationType.pop, direction:delta ?delta0 ?NavigationDirection.forward :NavigationDirection.back :NavigationDirection.unknown, }) }) } 纵观而视,popStateHandler在路由跳转时,做了这些事情: 更新history的location和state等信息,使得缓存信息同步;暂停监控时,中断跳转并重置pauseState;将必要信息告知所有注册的订阅者,并执行注册回调;beforeUnloadListenerfunctionbeforeUnloadListener(){ const{history}=window if(!history.state)return history.replaceState( assign({},history.state,{scroll:computeScrollPosition()}), '' ) } 关闭页面前会执行这个方法,主要作用是记录下当前页面滚动。 3个listener hooks//暂停监听 functionpauseListeners(){ pauseState=currentLocation.value } //注册监听逻辑 functionlisten(callback:NavigationCallback){ //setupthelistenerandprepareteardowncallbacks listeners.push(callback) constteardown=()={ constindex=listeners.indexOf(callback) if(index-1)listeners.splice(index,1) } teardowns.push(teardown) returnteardown } //监听器销毁 functiondestroy(){ for(constteardownofteardowns)teardown() teardowns=[] window.removeEventListener('popstate',popStateHandler) window.removeEventListener('beforeunload',beforeUnloadListener) } 添加location和state劫持Object.defineProperty(routerHistory,'location',{ enumerable:true, get:()=historyNavigation.location.value, }) Object.defineProperty(routerHistory,'state',{ enumerable:true, get:()=historyNavigation.state.value, }) 这里没啥好说的,就是读取routerHistory.location或routerHistory.state时能获取到historyNavigation方法中的内容。 到这里就是createWebHistory如何结合vue创建出一个router history的整个过程了。 createWebHashHistorycreateMemoryHistory主要创建一个基于内存的历史记录,这个历史记录的主要目的是处理 SSR。 其逻辑和createWebHistory大同小异,都是通过history和监听器实现,只不过在服务器场景中,没有window对象,也没法用到H5 History API能力,所以history用了一个queue(队列)代替,而监听器也是消费队列完成路由切换。以下是关键源码: /** *Createsanin-memorybasedhistory.ThemainpurposeofthishistoryistohandleSSR.Itstartsinaspeciallocationthatisnowhere. *It'suptotheusertoreplacethatlocationwiththestarterlocationbyeithercalling`router.push`or`router.replace`. * *@parambase-Baseappliedtoallurls,defaultsto'/' *@returnsahistoryobjectthatcanbepassedtotherouterconstructor */ exportfunctioncreateMemoryHistory(base:string=''):RouterHistory{ letlisteners:NavigationCallback[]=[] letqueue:HistoryLocation[]=[START] letposition:number=0 base=normalizeBase(base) //通过position(计步器)改变queue达到路由跳转效果 functionsetLocation(location:HistoryLocation){ position++ if(position===queue.length){ //weareattheend,wecansimplyappendanewentry queue.push(location) }else{ //weareinthemiddle,weremoveeverythingfromhereinthequeue queue.splice(position) queue.push(location) } } //监听器触发 functiontriggerListeners( to:HistoryLocation, from:HistoryLocation, {direction,delta}:PickNavigationInformation,'direction'|'delta' ):void{ constinfo:NavigationInformation={ direction, delta, type:NavigationType.pop, } for(constcallbackoflisteners){ callback(to,from,info) } } //构建routerhistory constrouterHistory:RouterHistory={ //rewrittenbyObject.defineProperty location:START, //TODO:shouldbekeptinqueue state:{}, base, createHref:createHref.bind(null,base), //replace方法 replace(to){ //removecurrententryanddecrementposition queue.splice(position--,1) setLocation(to) }, //push方法 //这2种方法都是调用setLocation来改变queue push(to,data?:HistoryState){ setLocation(to) }, //添加监听回调 listen(callback){ listeners.push(callback) return()={ constindex=listeners.indexOf(callback) if(index-1)listeners.splice(index,1) } }, destroy(){ listeners=[] queue=[START] position=0 }, go(delta,shouldTrigger=true){ constfrom=this.location constdirection:NavigationDirection= //weareconsideringdelta===0goingforward,butinabstractmode //using0forthedeltadoesn'tmakesenselikeitdoesinhtml5where //itreloadsthepage delta0?NavigationDirection.back:NavigationDirection.forward position=Math.max(0,Math.min(position+delta,queue.length-1)) if(shouldTrigger){ triggerListeners(this.location,from,{ direction, delta, }) } }, } //增加获取数据劫持 Object.defineProperty(routerHistory,'location',{ enumerable:true, get:()=queue[position], }) //针对单测时处理 if(__TEST__){ //... } returnrouterHistory } 落幕好了好了,这节先到这里,最后感谢大家阅览并欢迎纠错。 阅读原文

上一篇:2025-09-29_附下载|业内首份企业级智能体产业落地研究报告:从场景试点到规模化应用实践 下一篇:2019-04-10_TensorFlow 2.0到底怎么样?简单的图像分类任务探一探

TAG标签:

20
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价