小编在上一篇文章中提到,在vue-router中,mode参数是用来控制路由的实现方式的。今天,我们就来深入了解一下vue-router源码是如何实现路由的路由本源的——后端路由的概念最早出现在后端。以前用模板引擎开发页面的时候,经常看到这样一个地址http://www.vueRouter.com/login。大致流程可以看成这样:浏览器发起请求,服务端监听端口请求,比如(80,443)请求,解析URL路径,根据服务端的配置,返回相应的信息(html字符,json数据,图片...)浏览器根据数据包的Content-Type决定如何解析数据,即:路由是一种与后端服务器连接的交互方式,通过不同的方式请求不同的资源路径,请求不同的页面是路由的功能之一。前端路由随着前端应用的业务功能越来越复杂,用户对用户体验的要求越来越高,单页面应用(SPA)成为前端应用的主流形式。大型单页应用程序最显着的特点之一是使用前端路由系统通过更改URL来更新页面视图,而无需重新请求页面。“更新视图而不重新请求页面”是前端路由原理的核心之一。目前在浏览器环境下实现该功能主要有两种方式:hash模式:在浏览器中使用#history模式:使用HTML5中新方法hash模式hash示例中的History接口:http://www.vueRouter.com/loginhash方式:hash值是URL的锚点部分(#开头的部分)。hash值的变化不会导致浏览器向服务器发起请求,浏览器不会发起请求,所以界面不会刷新。另外,每次hash值发生变化,也会触发hashchange事件。通过这个事件,我们可以知道哈希值发生了哪些变化。那么我们可以监听hashchange来实现更新页面部分内容的操作:不想丑陋的hash,我们可以使用路由的history模式,它充分利用了history.pushStateAPI来完成URL跳转,而不需要重新加载页面。constrouter=newVueRouter({mode:'history',routes:[...]})源码分析找到VueRouter类的定义,mode参数相关摘录如下:exportdefaultclassVueRouter{模式:字符串;//传入的字符串参数表示历史类history:HashHistory|HTML5历史|摘要历史;//真正起作用的对象属性必须是以上三个类的枚举fallback:boolean;//如果浏览器不支持,'history'模式需要回滚到'hash'模式构造器(options:RouterOptions={}){letmode=options.mode||'hash'//默认为'hash'模式this.fallback=mode==='history'&&!supportsPushState//使用supportsPushState判断浏览器是否支持'history'模式if(this.fallback){mode='hash'}if(!inBrowser){mode='abstract'//不在浏览器环境下运行强制为'abstract'模式}this.mode=mode//根据mode判断history的实际类并实例化开关(模式){case'history':this.history=newHTML5History(this,options.base)breakcase'hash':this.history=newHashHistory(this,options.base,this.fallback)breakcase'abstract':这个.history=newAbstractHistory(this,options.base)breakdefault:if(process.env.NODE_ENV!=='production'){assert(false,`invalidmode:${mode}`)}}}init(app:any/*Vue组件实例*/){consthistory=this.history//根据history的类别进行相应的初始化操作和监听if(historyinstanceofHTML5History){history.transitionTo(history.getCurrentLocation())}elseif(historyinstanceofHashHistory){constsetupHashListener=()=>{history.setupListeners()}history.transitionTo(history.getCurrentLocation(),setupHashListener,setupHashListener)}history.listen(route=>{this.apps.forEach((app)=>{app._route=route})})}//VueRouter类暴露的以下方法实际上调用了具体history对象的方法push(location:RawLocation,onComplete?:Function,onAbort?:Function){this.history.push(location,onComplete,onAbort)}replace(locationn:RawLocation,onComplete?:Function,onAbort?:Function){this.history.replace(location,onComplete,onAbort)}}作为参数传入的字符串属性mode只是一个flag来表示实际工作的对象之前history属性的实现类初始化对应的history,会检查mode:如果浏览器不支持HTML5History方法(通过supportsPushState变量判断),mode强制为'hash';如果不是在浏览器环境下运行,则模式强制为'abstract'。VueRouter类中的onReady()、push()等方法只是一个代理,实际上是具体调用的历史对象对应的方法。在init()方法中初始化时,也是根据history在浏览器环境中对特定类别的对象进行不同操作的两种方式,分别在HTML5History和HashHistory中实现。它们定义在src/history文件夹中,并继承自同一目录中的base.js文件。定义的历史类。public和basic方法定义在History中,直接看会比较混乱。让我们从HTML5History和HashHistory这两个类中友好的push()和replace()方法开始。HashHistory.push()首先我们看一下HashHistory中的push()方法:)onComplete&&onComplete(route)},onAbort)}functionpushHash(path){window.location.hash=path}transitionTo()方法定义在父类中,用于处理路由变化中的基本逻辑,push()方法是直接赋值窗口的hash:window.location.hash=route.fullPathhash变化会自动添加到浏览器的访问历史中。那么视图的更新是如何实现的呢?我们看一下父类History中的transitionTo()方法:route,()=>{this.updateRoute(route)...})}updateRoute(route:Route){this.cb&&this.cb(route)}listen(cb:Function){this.cb=cb}可以看到当路由发生变化时,会调用History中的this.cb方法,通过History.listen(cb)设置this.cb方法。回到VueRouter类定义,发现在init()方法中设置了:init(app:any/*Vue组件实例*/){this.apps.push(app)history.listen(route=>{this.apps.forEach((app)=>{app._route=route})})}根据注释,app是一个Vue组件实例,但是我们知道Vue作为一个渐进式前端框架,应该定义在其自身的组件中并没有内置属性_route用于路由。如果组件中有这样的属性,应该是在插件加载的地方,也就是Vue对象混入了VueRouter的install()方法中。查看install.js的源码,有如下一段话:$options.routerthis._router.init(this)Vue.util.defineReactive(this,'_route',this._router.history.current)}registerInstance(this,this)},})}通过Vue.mixin()方法,全局注册一个mix,影响注册后创建的所有Vue实例,mix在beforeCreatehook中通过Vue.util.defineReactive()定义responsive_route属性。所谓responsive属性就是当_route值发生变化时,会自动调用Vue实例的render()方法来更新视图。总结一下,从设置路由变化到查看更新的过程如下:$router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()HashHistory.replace()replace()方法和push()方法的区别在于它不会在栈顶添加新的路由浏览器访问历史记录,但将其替换当前路由:replace(location:RawLocation,onComplete?:Function,onAbort?:Function){this.transitionTo(location,route=>{replaceHash(route.fullPath)onComplete&&onComplete(route)},onAbort)}functionreplaceHash(path){consti=window.location.href.indexOf('#')window.location.replace(window.location.href.slice(0,i>=0?i:0)+'#'+path)}听地址栏上面讨论的VueRouter.push()和VueRouter.replace()可以在vue组件的逻辑代码中直接调用。另外,在浏览器中,用户也可以直接在浏览器地址栏中输入改变路由,所以VueRouter也需要能够监听浏览器地址栏中路由的变化,并且有和调用一样的响应行为通过代码。在HashHistory中,这个功能是通过setupListeners实现的:setupListeners(){window.addEventListener('hashchange',()=>{if(!ensureSlash()){return}this.transitionTo(getHash(),route=>{replaceHash(route.fullPath)})})}该方法监听浏览器事件hashchange,调用的函数为replaceHash,即直接在浏览器地址栏输入路由,相当于调用HTML5HistoryHistory代码中的replace()方法interface是浏览器历史栈提供的接口,通过back()、forward()、go()等方法,我们可以读取浏览器历史栈的信息,进行各种跳转操作。window.history.pushState(stateObject,title,URL)window.history.replaceState(stateObject,title,URL)我们来看vue-router中的源码:push(location:RawLocation,onComplete?:Function,onAbort?:Function){const{current:fromRoute}=thisthis.transitionTo(location,route=>{pushState(cleanPath(this.base+route.fullPath))handleScroll(this.router,route,fromRoute,false)onComplete&&onComplete(route)},onAbort)}replace(location:RawLocation,onComplete?:Function,onAbort?:Function){const{current:fromRoute}=thisthis.transitionTo(location,route=>{replaceState(cleanPath(this.base+route.fullPath))handleScroll(this.router,route,fromRoute,false)onComplete&&onComplete(route)},onAbort)}//src/util/push-state.jsexport函数pushState(url?:string,replace?:boolean){saveScrollPosition()//尝试...捕获pushState调用以绕过Safari//DOM异常18限制到100次pushState调用consthistory=window.historytry{if(replace){history.replaceState({key:_key},'',url)}else{_key=genKey()history.pushState({key:_key},'',url)}}catch(e){window.location[替换?'replace':'assign'](url)}}exportfunctionreplaceState(url?:string){pushState(url,true)}HTML5History中修改浏览器地址栏URL的监听器直接在构造函数中执行:constructor(router:Router,base:?string){window.addEventListener('popstate',e=>{constcurrent=this.currentthis.transitionTo(getLocation(this.base),route=>{if(expectScroll){handleScroll(router,route,current,true)}})})}当然,HTML5History使用了HTML5的新特性,需要特定的浏览器版本支持,我们已经知道,浏览器是否支持是通过变量supportsPushState来检测的://src/util/push-state.jsexportconstsupportsPushState=inBrowser&&(function(){constua=window.navigator.userAgentif((ua.indexOf('Android2.')!==-1||ua.indexOf('安卓d4.0')!==-1)&&ua.indexOf('MobileSafari')!==-1&&ua.indexOf('Chrome')===-1&&ua.indexOf('WindowsPhone')===-1){returnfalse}returnwindow.history&&'pushState'inwindow.history})()两种模式的比较根据MDN的介绍,调用history.pushState()相比直接修改有如下优势hash:pushState设置的新URL可以是任意一个与当前URL同源的URL;而hash只能修改#后面的部分,所以只能和同一个文档的当前url设置。pushState设置的新URL可以和当前URL完全一样,所以它也会往栈中添加记录;并且hash设置的新值必须与原始值不同才能触发将记录添加到堆栈中。pushState可以通过stateObject向记录中添加任意类型的数据;而hash只能加短字符串。pushState可以额外设置title属性,方便后续使用AbstractHistory抽象方式是最简单的过程,因为不涉及关联浏览器地址相关记录;整体流程还是和HashHistory一样,只是这里通过一个数组信息来模拟浏览器历史栈导出类AbstractHistory扩展历史{索引:数字;stack:Array
