vue-router相关基础知识及工作原理
前言
今天面试被问到vue的动态路由,我竟然没有回答上来,感觉不是什么难得问题。好久没有看vue-router的文档,很多用的东西和概念没有对上。回来一看什么是动态路由就傻眼了。看来有必要把vue-router相关知识总结一下,好丢人的感觉。
单页面应用的工作原理
我理解的单页面工作原理是通过浏览器URL的#后面的hash变化就会引起页面变化的特性来把页面分成不同的小模块,然后通过修改hash来让页面展示我们想让看到的内容。
那么为什么hash的不同,为什么会影响页面的展示呢?浏览器在这里面做了什么内容。以前#后面的内容一般会做锚点,但是会定位到一个页面的某个位置,这个是怎么做到的呢,和我们现在的路由有什么不同。(我能想到一个路由的展示就会把其他路由隐藏,是这样的吗)后面会看一看写一下这个疑惑,现在最重要的是先把基本概念弄熟。
正文
当你要把vue-router添加进来,我们需要做的是,将组件(components)映射到路由(routes),然后告诉vue-router在哪里渲染它们
起步
//***router-link告诉浏览器去哪个路由 //***router-view告诉路由在哪里展示内容HelloApp!
GotoFoo GotoBar
动态路由匹配
相当于同一个组件,因为参数不同展示不同的组件内容,其实就是在vue-router的路由路径中使用『动态路径参数』
constrouter=newVueRouter({ routes:[ //动态路径参数以冒号开头 {path:'/user/:id',component:User} ] })
那么我们进入uesr/001和user/002其实是进入的同一个路由,可以根据参数的不同在内容页展示不同的内容。一般适用场景:列表,权限控制
定义的时候用:表示是动态路由
使用{{$route.params.id}}来拿到本路由里面参数的内容
当使用路由参数时,例如从/user/foo导航到/user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
复用组件时,想对路由参数的变化作出响应的话,你可以简单地watch(监测变化)$route对象
constUser={ template:'...', watch:{ '$route'(to,from){ //对路由变化作出响应... } } }
有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高。
嵌套路由
在路由里面嵌套一个路由
//路由里面也会出现这是嵌套路由展示内容的地方 constUser={ template:` User{{$route.params.id}}
设置空路由,在没有指定路由的时候就会展示空路由内容
constrouter=newVueRouter({ routes:[ { path:'/user/:id',component:User, children:[ //当/user/:id匹配成功, //UserHome会被渲染在User的中 {path:'',component:UserHome}, ] } ] })
编程式导航
声明式:
编程式:router.push(...)
可以想象编程式push可以理解为向浏览器历史里面push一个新的hash,导致路由发生变化
router.replace() 修改路由但是不存在历史里面
router.go(n) 有点像JS的window.history.go(n)
命名路由就是给每一个路由定义一个名字。
命名视图
有时候想同时(同级)展示多个视图,而不是嵌套展示,例如创建一个布局,有sidebar(侧导航)和main(主内容)两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果router-view没有设置名字,那么默认为default。
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用components配置(带上s):
constrouter=newVueRouter({ routes:[ { path:'/', components:{ default:Foo, a:Bar, b:Baz } } ] })
重定向和别名
重定向也是通过routes配置来完成,下面例子是从/a重定向到/b:
constrouter=newVueRouter({ routes:[ {path:'/a',redirect:'/b'} ] })
一般首页的时候可以重定向到其他的地方
重定向的目标也可以是一个命名的路由:
constrouter=newVueRouter({ routes:[ {path:'/a',redirect:{name:'foo'}} ] })
甚至是一个方法,动态返回重定向目标:
constrouter=newVueRouter({ routes:[ {path:'/a',redirect:to=>{ //方法接收目标路由作为参数 //return重定向的字符串路径/路径对象 }} ] })
『重定向』的意思是,当用户访问/a时,URL将会被替换成/b,然后匹配路由为/b,那么『别名』又是什么呢?
/a的别名是/b,意味着,当用户访问/b时,URL会保持为/b,但是路由匹配则为/a,就像用户访问/a一样。
上面对应的路由配置为:
constrouter=newVueRouter({ routes:[ {path:'/a',component:A,alias:'/b'} ] })
『别名』的功能让你可以自由地将UI结构映射到任意的URL,而不是受限于配置的嵌套路由结构。
HTML5History模式
ue-router默认hash模式——使用URL的hash来模拟一个完整的URL,于是当URL改变时,页面不会重新加载。
如果不想要很丑的hash,我们可以用路由的history模式,这种模式充分利用history.pushStateAPI来完成URL跳转而无须重新加载页面。
constrouter=newVueRouter({ mode:'history', routes:[...] })
当你使用history模式时,URL就像正常的url,例如http://yoursite.com/user/id,也好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问http://oursite.com/user/id就会返回404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果URL匹配不到任何静态资源,则应该返回同一个index.html页面,这个页面就是你app依赖的页面。
给个警告,因为这么做以后,你的服务器就不再返回404错误页面,因为对于所有路径都会返回index.html文件。为了避免这种情况,你应该在Vue应用里面覆盖所有的路由情况,然后在给出一个404页面。
constrouter=newVueRouter({ mode:'history', routes:[ {path:'*',component:NotFoundComponent} ] })
或者,如果你使用Node.js服务器,你可以用服务端路由匹配到来的URL,并在没有匹配到路由的时候返回404,以实现回退。
导航守卫
我的理解就是组件或者全局级别的组件的钩子函数
正如其名,vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的,单个路由独享的,或者组件级的。
记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察$route对象来应对这些变化,或使用beforeRouteUpdate的组件内守卫。
全局守卫
constrouter=newVueRouter({...}) router.beforeEach((to,from,next)=>{ //... })
每个守卫方法接收三个参数:
to:Route:即将要进入的目标路由对象
from:Route:当前导航正要离开的路由
next:Function:一定要调用该方法来resolve这个钩子。执行效果依赖next方法的调用参数。
next():进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed(确认的)。
next(false):中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址。
next(‘/')或者next({path:‘/'}):跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next传递任意位置对象,且允许设置诸如replace:true、name:‘home'之类的选项以及任何用在router-link的toprop或router.push中的选项。
next(error):(2.4.0+)如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调。
确保要调用next方法,否则钩子就不会被resolved。
全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受next函数也不会改变导航本身:
router.afterEach((to,from)=>{ //... })
路由独享的守卫
你可以在路由配置上直接定义beforeEnter守卫:
constrouter=newVueRouter({ routes:[ { path:'/foo', component:Foo, beforeEnter:(to,from,next)=>{ //... } } ] })
这些守卫与全局前置守卫的方法参数是一样的。
组件内的守卫
最后,你可以在路由组件内直接定义以下路由导航守卫:
beforeRouteEnter beforeRouteUpdate(2.2新增) beforeRouteLeave constFoo={ template:`...`, beforeRouteEnter(to,from,next){ //在渲染该组件的对应路由被confirm前调用 //不!能!获取组件实例`this` //因为当守卫执行前,组件实例还没被创建 }, beforeRouteUpdate(to,from,next){ //在当前路由改变,但是该组件被复用时调用 //举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候, //由于会渲染同样的Foo组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 //可以访问组件实例`this` }, beforeRouteLeave(to,from,next){ //导航离开该组件的对应路由时调用 //可以访问组件实例`this` } }
beforeRouteEnter守卫不能访问this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
完整的导航解析流程
导航被触发。
在失活的组件里调用离开守卫。
调用全局的beforeEach守卫。
在重用的组件里调用beforeRouteUpdate守卫(2.2+)。
在路由配置里调用beforeEnter。
解析异步路由组件。
在被激活的组件里调用beforeRouteEnter。
调用全局的beforeResolve守卫(2.5+)。
导航被确认。
调用全局的afterEach钩子。
触发DOM更新。
用创建好的实例调用beforeRouteEnter守卫中传给next的回调函数。
路由元信息
我的理解就是他可以把路由的父路径都列举出来,完成一些任务,比如登录,user组件需要登录,那么user下面的foo组件也需要,那么可以通过这个属性来检测这个路由线上的一些状态。
定义路由的时候可以配置meta字段:
constrouter=newVueRouter({ routes:[ { path:'/foo', component:Foo, children:[ { path:'bar', component:Bar, //ametafield meta:{requiresAuth:true} } ] } ] })
首先,我们称呼routes配置中的每个路由对象为路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录
例如,根据上面的路由配置,/foo/bar这个URL将会匹配父路由记录以及子路由记录。
一个路由匹配到的所有路由记录会暴露为$route对象(还有在导航守卫中的路由对象)的$route.matched数组。因此,我们需要遍历$route.matched来检查路由记录中的meta字段。
下面例子展示在全局导航守卫中检查元字段:
router.beforeEach((to,from,next)=>{ if(to.matched.some(record=>record.meta.requiresAuth)){ //thisrouterequiresauth,checkifloggedin //ifnot,redirecttologinpage. if(!auth.loggedIn()){ next({ path:'/login', query:{redirect:to.fullPath} }) }else{ next() } }else{ next()//确保一定要调用next() } })
数据获取
我的理解就是在哪里获取数据,可以再组件里面,也可以在组件的守卫里面,也就是组件的生命周期里面。
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:
导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示『加载中』之类的指示。
导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
从技术角度讲,两种方式都不错——就看你想要的用户体验是哪种。
导航完成后获取数据
当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的created钩子中获取数据。这让我们有机会在数据获取期间展示一个loading状态,还可以在不同视图间展示不同的loading状态。
假设我们有一个Post组件,需要基于$route.params.id获取文章数据:
Loading...