浅析vue-router实现原理及两种模式
之前用Vue开发单页应用,发现不管路由怎么变化,浏览器地址栏总是会有一个'#'号。
当时检查自己的代码,没有发现请求的地址带'#',当时也很纳闷,但是由于没有影响页面的渲染以及向后台发送请求,当时也没有在意。最近看了一下vue-router的实现原理,才逐渐揭开了这个谜题。
vue-router的两种方式(浏览器环境下)
1.Hash(对应HashHistory)
hash(“#”)符号的本来作用是加在URL中指示网页中的位置:
http://www.example.com/index.html#print
#符号本身以及它后面的字符称之为hash(也就是我之前为什么地址栏都会有一个‘#'),可通过window.location.hash属性读取。它具有如下特点:
hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面
2.可以为hash的改变添加监听事件:
window.addEventListener("hashchange",funcRef,false)
每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录
利用hash的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了。
2.History(对应HTML5History)
History接口是浏览器历史记录栈提供的接口,通过back(),forward(),go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。
从HTML5开始,Historyinterface提供了两个新的方法:pushState(),replaceState()使得我们可以对浏览器历史记录栈进行修改:
window.history.pushState(stateObject,title,URL) window.history.replaceState(stateObject,title,URL)
stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本title:所添加记录的标题URL:所添加记录的URL
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。浏览器历史记录可以看作一个「栈」。栈是一种后进先出的结构,可以把它想象成一摞盘子,用户每点开一个新网页,都会在上面加一个新盘子,叫「入栈」。用户每次点击「后退」按钮都会取走最上面的那个盘子,叫做「出栈」。而每次浏览器显示的自然是最顶端的盘子的内容。
vue-router的作用
vue-router的作用就是通过改变URL,在不重新请求页面的情况下,更新页面视图。简单的说就是,虽然地址栏的地址改变了,但是并不是一个全新的页面,而是之前的页面某些部分进行了修改。
exportdefaultnewRouter({ //mode:'history',//后端支持可开 routes:constantRouterMap })
这是Vue项目中常见的一段初始化vue-router的代码,之前没仔细研究过vue-router,不知道还有一个mode属性,后来看了相关文章后了解到,mode属性用来指定vue-router使用哪一种模式。在没有指定mode的值,则使用hash模式。
源码分析
首先看一下vue-router的构造函数
constructor(options:RouterOptions={}){ this.app=null this.apps=[] this.options=options this.beforeHooks=[] this.resolveHooks=[] this.afterHooks=[] this.matcher=createMatcher(options.routes||[],this) letmode=options.mode||'hash' this.fallback=mode==='history'&&!supportsPushState&&options.fallback!==false if(this.fallback){ mode='hash' } if(!inBrowser){ mode='abstract' } this.mode=mode switch(mode){ case'history': this.history=newHTML5History(this,options.base) break case'hash': this.history=newHashHistory(this,options.base,this.fallback) break case'abstract'://非浏览器环境下 this.history=newAbstractHistory(this,options.base) break default: if(process.env.NODE_ENV!=='production'){ assert(false,`invalidmode:${mode}`) } } }
主要是先获取mode的值,如果mode的值为history但是浏览器不支持history模式,那么就强制设置mode值为hash。如果支持则为history。接下来,根据mode的值,来选择vue-router使用哪种模式。
case'history': this.history=newHTML5History(this,options.base) break case'hash': this.history=newHashHistory(this,options.base,this.fallback) break
这样就有了两种模式。确定好了vue-router使用哪种模式后,就到了init。先来看看router的init方法就干了哪些事情,在src/index.js中
init(app:any/*Vuecomponentinstance*/){ //.... consthistory=this.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(location:RawLocation,onComplete?:Function,onAbort?:Function){ this.history.replace(location,onComplete,onAbort) } }
如果是HTML5History,则执行
history.transitionTo(history.getCurrentLocation())
如果是Hash模式,则执行
constsetupHashListener=()=>{ history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener )
可以看出,两种模式都执行了transitionTo()函数。接下来看一下两种模式分别是怎么执行的,首先看一下Hash模式
HashHistory.push()
我们来看HashHistory中的push()方法:
push(location:RawLocation,onComplete?:Function,onAbort?:Function){ this.transitionTo(location,route=>{ pushHash(route.fullPath) onComplete&&onComplete(route) },onAbort) } functionpushHash(path){ window.location.hash=path }
transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对window的hash进行了直接赋值:
window.location.hash=route.fullPathhash的改变会自动添加到浏览器的访问历史记录中。
那么视图的更新是怎么实现的呢,我们来看父类History中transitionTo()方法的这么一段:
transitionTo(location:RawLocation,onComplete?:Function,onAbort?:Function){ //调用match得到匹配的route对象 constroute=this.router.match(location,this.current) this.confirmTransition(route,()=>{ this.updateRoute(route) ... }) } updateRoute(route:Route){ this.cb&&this.cb(route) } listen(cb:Function){ this.cb=cb }
可以看到,当路由变化时,调用了History中的this.cb方法,而this.cb方法是通过History.listen(cb)进行设置的。回到VueRouter类定义中,找到了在init()方法中对其进行了设置:
init(app:any/*Vuecomponentinstance*/){ this.apps.push(app) history.listen(route=>{ this.apps.forEach((app)=>{ app._route=route }) }) }
代码中的app指的是Vue的实例,._route本不是本身的组件中定义的内置属性,而是在Vue.use(Router)加载vue-router插件的时候,通过Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个Vue实例,该混合在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的_route。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。vm.render()是根据当前的_route的path,name等属性,来将路由对应的组件渲染到.所以总结下来,从路由改变到视图的更新流程如下:
this.$router.push(path) --> HashHistory.push() --> History.transitionTo() --> constroute=this.router.match(location,this.current)会进行地址匹配,得到一个对应当前地址的route(路由信息对象) --> History.updateRoute(route) --> app._route=route(Vue实例的_route改变)由于_route属性是采用vue的数据劫持,当_route的值改变时,会执行响应的render() --> vm.render()具体是在中render --> window.location.hash=route.fullpath(浏览器地址栏显示新的路由的path)
HashHistory.replace()
说完了HashHistory.push(),该说HashHistory.replace()了。
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 ) }
可以看出来,HashHistory.replace它与push()的实现结构上基本相似,不同点在于它不是直接对window.location.hash进行赋值,而是调用window.location.replace方法将路由进行替换。这样不会将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由。
监听地址栏
可以看出来,上面的过程都是在代码内部进行路由的改变的,比如项目中常见的this.$router.push(),等方法。然后将浏览器的地址栏置为新的hash值。那么如果直接在地址栏中输入URL从而改变路由呢,例如
我将dashboadr删除,然后置为article/hotSpot,然后回车,vue又是如何处理的呢?
setupListeners(){ window.addEventListener('hashchange',()=>{ if(!ensureSlash()){ return } this.transitionTo(getHash(),route=>{ replaceHash(route.fullPath) }) }) }
该方法设置监听了浏览器事件hashchange,调用的函数为replaceHash,即在浏览器地址栏中直接输入路由相当于代码调用了replace()方法.后面的步骤自然与HashHistory.replace()相同,一样实现页面渲染。
HTML5History
HTML5History模式的vue-router代码结构以及更新视图的逻辑与hash模式基本类似,和HashHistory的步骤基本一致,只是HashHistory的push和replace()变成了HTML5History.pushState()和HTML5History.replaceState()
在HTML5History中添加对修改浏览器地址栏URL的监听是直接在构造函数中执行的,对HTML5History的popstate事件进行监听:
constructor(router:Router,base:?string){ window.addEventListener('popstate',e=>{ constcurrent=this.current this.transitionTo(getLocation(this.base),route=>{ if(expectScroll){ handleScroll(router,route,current,true) } }) }) }
以上就是vue-routerhash模式与history模式不同模式下处理逻辑的分析了。
总结
以上所述是小编给大家介绍的vue-router实现原理及两种模式分析,希望对大家有所帮助!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。