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...