简化版的vue-router实现思路详解
本文旨在介绍vue-router的实现思路,并动手实现一个简化版的vue-router。我们先来看一下一般项目中对vue-router最基本的一个使用,可以看到,这里定义了四个路由组件,我们只要在根vue实例中注入该router对象就可以使用了.
importVueRouterfrom'vue-router'; importHomefrom'@/components/Home'; importAfrom'@/components/A'; importBfrom'@/components/B' importCfrom'@/components/C' Vue.use(VueRouter) exportdefaultnewVueRouter.Router({ //mode:'history', routes:[ { path:'/', component:Home }, { path:'/a', component:A }, { path:'/b', component:B }, { path:'/c', component:C } ] })
vue-router提供两个全局组件,router-view和router-link,前者是用于路由组件的占位,后者用于点击时跳转到指定路由。此外组件内部可以通过this.$router.push,this.$rouer.replace等api实现路由跳转。本文将实现上述两个全局组件以及push和replace两个api,调用的时候支持params传参,并且支持hash和history两种模式,忽略其余api、嵌套路由、异步路由、abstract路由以及导航守卫等高级功能的实现,这样有助于理解vue-router的核心原理。本文的最终代码不建议在生产环境使用,只做一个学习用途,下面我们就来一步步实现它。
install实现
任何一个vue插件都要实现一个install方法,通过Vue.use调用插件的时候就是在调用插件的install方法,那么路由的install要做哪些事情呢?首先我们知道我们会用new关键字生成一个router实例,就像前面的代码实例一样,然后将其挂载到根vue实例上,那么作为一个全局路由,我们当然需要在各个组件中都可以拿到这个router实例。另外我们使用了全局组件router-view和router-link,由于install会接收到Vue构造函数作为实参,方便我们调用Vue.component来注册全局组件。因此,在install中主要就做两件事,给各个组件都挂载router实例,以及实现router-view和router-link两个全局组件。下面是代码:
constinstall=(Vue)=>{ if(this._Vue){ return; }; Vue.mixin({ beforeCreate(){ if(this.$options&&this.$options.router){ this._routerRoot=this; this._router=this.$options.router; Vue.util.defineReactive(this,'_routeHistory',this._router.history) }else{ this._routerRoot=(this.$parent&&this.$parent._routerRoot)||this } Object.defineProperty(this,'$router',{ get(){ returnthis._routerRoot._router; } }) Object.defineProperty(this,'$route',{ get(){ return{ current:this._routerRoot._routeHistory.current, ...this._routerRoot._router.route }; } }) } }); Vue.component('router-view',{ render(h){...} }) Vue.component('router-link',{ props:{ to:String, tag:String, }, render(h){...} }) this._Vue=Vue; }
这里的this代表的就是vue-router对象,它有两个属性暴露出来供外界调用,一个是install,一个是Router构造函数,这样可以保证插件的正确安装以及路由实例化。我们先忽略Router构造函数,来看install,上面代码中的this._Vue是个开始没有定义的属性,他的目的是防止多次安装。我们使用Vue.mixin对每个组件的beforeCreate钩子做全局混入,目的是让每个组件实例共享router实例,即通过this.$router拿到路由实例,通过this.$route拿到路由状态。需要重点关注的是这行代码:
Vue.util.defineReactive(this,'_routeHistory',this._router.history)
这行代码利用vue的响应式原理,对根vue实例注册了一个_routeHistory属性,指向路由实例的history对象,这样history也变成了响应式的。因此一旦路由的history发生变化,用到这个值的组件就会触发render函数重新渲染,这里的组件就是router-view。从这里可以窥察到vue-router实现的一个基本思路。上述的代码中对于两个全局组件的render函数的实现,因为会依赖于router对象,我们先放一放,稍后再来实现它们,下面我们分析一下Router构造函数。
Router构造函数
经过刚才的分析,我们知道router实例需要有一个history对象,需要一个保存当前路由状态的对象route,另外很显然还需要接受路由配置表routes,根据routes需要一个路由映射表routerMap来实现组件搜索,还需要一个变量mode判断是什么模式下的路由,需要实现push和replace两个api,代码如下:
constRouter=function(options){ this.routes=options.routes;//存放路由配置 this.mode=options.mode||'hash'; this.route=Object.create(null),//生成路由状态 this.routerMap=createMap(this.routes)//生成路由表 this.history=newRouterHistory();//实例化路由历史对象 this.init();//初始化 } Router.prototype.push=(options)=>{...} Router.prototype.replace=(options)=>{...} Router.prototype.init=()=>{...}
我们看一下路由表routerMap的实现,由于不考虑嵌套等其他情况,实现很简单,如下:
constcreateMap=(routes)=>{ letresMap=Object.create(null); routes.forEach(route=>{ resMap[route['path']]=route['component']; }) returnresMap; }
RouterHistory的实现也很简单,根据前面分析,我们只需要一个current属性就可以,如下:
constRouterHistory=function(mode){ this.current=null; }
有了路由表和history,router-view的实现就很容易了,如下:
Vue.component('router-view',{ render(h){ letrouterMap=this._self.$router.routerMap; returnh(routerMap[this._self.$route.current]) } })
这里的this是一个renderProxy实例,他有一个属性_self可以拿到当前的组件实例,进而访问到routerMap,可以看到路由实例history的current本质上就是我们配置的路由表中的path。
接下来我们看一下Router要做哪些初始化工作。对于hash路由而言,url上hash值的改变不会引起页面刷新,但是可以触发一个hashchange事件。由于路由history.current初始为null,因此匹配不到任何一个路由,所以会导致页面刷新加载不出任何路由组件。基于这两点,在init方法中,我们需要实现对页面加载完成的监听,以及hash变化的监听。对于history路由,为了实现浏览器前进后退时准确渲染对应组件,还要监听一个popstate事件。代码如下:
Router.prototype.init=function(){ if(this.mode==='hash'){ fixHash() window.addEventListener('hashchange',()=>{ this.history.current=getHash(); }) window.addEventListener('load',()=>{ this.history.current=getHash(); }) } if(this.mode==='history'){ removeHash(this); window.addEventListener('load',()=>{ this.history.current=location.pathname; }) window.addEventListener('popstate',(e)=>{ if(e.state){ this.history.current=e.state.path; } }) } }
当启用hash模式的时候,我们要检测url上是否存在hash值,没有的话强制赋值一个默认path,hash路由时会根据hash值作为key来查找路由表。fixHash和getHash实现如下:
constfixHash=()=>{ if(!location.hash){ location.hash='/'; } } constgetHash=()=>{ returnlocation.hash.slice(1)||'/'; }
这样在刷新页面和hash改变的时候,current可以得到赋值和更新,页面能根据hash值准确渲染路由。history模式也是一样的道理,只是它通过location.pathname作为key搜索路由组件,另外history模式需要去除url上可能存在的hash,removeHash实现如下:
constremoveHash=(route)=>{ leturl=location.href.split('#')[1] if(url){ route.current=url; history.replaceState({},null,url) } }
我们可以看到当浏览器后退的时候,history模式会触发popstate事件,这个时候是通过state状态去获取path的,那么state状态从哪里来呢,答案是从window.history对象的pushState和replaceState而来,这两个方法正好可以用来实现router的push方法和replace方法,我们看一下这里它们的实现:
Router.prototype.push=(options)=>{ this.history.current=options.path; if(this.mode==='history'){ history.pushState({ path:options.path },null,options.path); }elseif(this.mode==='hash'){ location.hash=options.path; } this.route.params={ ...options.params } } Router.prototype.replace=(options)=>{ this.history.current=options.path; if(this.mode==='history'){ history.replaceState({ path:options.path },null,options.path); }elseif(this.mode==='hash'){ location.replace(`#${options.path}`) } this.route.params={ ...options.params } }
pushState和replaceState能够实现改变url的值但不引起页面刷新,从而不会导致新请求发生,pushState会生成一条历史记录而replaceState不会,后者只是替换当前url。在这两个方法执行的时候将path存入state,这就使得popstate触发的时候可以拿到路径从而触发组件渲染了。我们在组件内按照如下方式调用,会将params写入router实例的route属性中,从而在跳转后的组件B内通过this.$route.params可以访问到传参。
this.$router.push({ path:'/b', params:{ id:55 } });
router-link实现
router-view的实现很简单,前面已经说过。最后,我们来看一下router-link的实现,先放上代码:
Vue.component('router-link',{ props:{ to:String, tag:String, }, render(h){ letmode=this._self.$router.mode; lettag=this.tag||'a'; letrouterHistory=this._self.$router.history; returnh(tag,{ attrs:tag==='a'?{ href:mode==='hash'?'#'+this.to:this.to, }:{}, on:{ click:(e)=>{ if(this.to===routerHistory.current){ e.preventDefault(); return; } routerHistory.current=this.to; switch(mode){ case'hash': if(tag==='a')return; location.hash=this.to; break; case'history': history.pushState({ path:this.to },null,this.to); break; default: } e.preventDefault(); } }, style:{ cursor:'pointer' } },this.$slots.default) } })
router-link可以接受两个属性,to表示要跳转的路由路径,tag表示router-link要渲染的标签名,默认为标签。如果是a标签,我们为其添加一个href属性。我们给标签绑定click事件,如果检测到本次跳转为当前路由的话什么都不做直接返回,并且阻止默认行为,否者根据to更换路由。hash模式下并且是a标签时候可以直接利用浏览器的默认行为完成url上hash的替换,否者重新为location.hash赋值。history模式下则利用pushState去更新url。
以上实现就是一个简单的vue-router,完整代码参见vue-router-simple。
总结
以上所述是小编给大家介绍的简化版的vue-router实现思路详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!