导航守卫
VueRouter提供的导航守卫主要用于在导航的过程中重定向或取消路由、或者添加权限验证、数据获取等业务逻辑。
导航守卫分为三类:全局守卫、路由独享守卫和组件内守卫。可以用于路由导航过程中的不同阶段。
每一个导航守卫都有三个参数:to、from和next。
- to:表示即将进入的目标路由对象
- from:当前导航正要离开的路由对象
- next:函数,以下是next的常用方法
- next(): 进行管道中的下一个钩子
- next(false): 中断当前导航
- next( ./xx ): 中断当前导航,并跳转至设置好的路径
1.全局守卫
全局守卫分为全局前置守卫、全局解析守卫和全局后置钩子。
1.1 全局前置守卫
当一个导航触发时,全局前置守卫按照创建的顺序调用。守卫可以是异步解析执行,此时导航在所有守卫解析完之前一直处于挂起状态。全局前置守卫使用router.beforeEach()注册。
const router = new VueRouter({....});
router.beforeEach((to,from,next)=>{
//这里执行具体操作
//next 调用
})
在使用全局前置守卫时,要确保next函数的正确调用。例如,下面就是一个错误的示例。
router.beforeEach((to,from,next)=>{
if(!localStorage.getItem('token'))next('/login');
//如果用户没有验证,next函数被调用两次
next();
})
正确的做法是:
router.beforeEach((to,from,next)=>{
if(!localStorage.getItem('token'))next('/login');
//如果用户没有验证,next函数被调用两次
else next();
})
1.2 全局解析守卫
全局解析守卫是vue-router2.5.0版本新增的,使用【router.beforeResolve】注册。它和【router.beforeEach】类似,区域在于,在导航被确认之前,在所有组件内守卫和异步路由组件被解析之后,解析守卫被调用。
const router = new VueRouter({....});
router.beforeResolve((to,from,next)=>{
//这里执行具体操作
//next 调用
})
1.3 全局后置钩子
全局后置钩子使用【router.afterEach】注册,它在导航被确认之后调用。
const router = new VueRouter({....});
router.afterEach((to,from)=>{
//这里执行具体操作
})
1.4 案例:登录验证
对于受保护的资源,我们需要用户登录后才能访问。如果用户没有登录,那么就将用户导航到登录页面。为此,可以利用全局前置守卫来完成用户登录与否的判断。
在components目录下新建login.vue。
<template>
<div>
<h3>{{ info }}</h3>
<table>
<caption>用户登录</caption>
<tbody>
<tr>
<td><label for="username">用户名:</label></td>
<td><input id="username" v-model.trim="username" placeholder="请输入用户名"/></td>
</tr>
<tr>
<td><label for="password">密码:</label></td>
<td><input id="password" v-model.trim="password" type="password" placeholder="请输入密码"/></td>
</tr>
<tr>
<td cols="2">
<input type="submit" value="登录" @click.prevent="login"/>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
data(){
return {
username: "",
password: "",
info: "" // 用于保存登录失败后的提示信息
}
},
methods: {
login() {
// 实际场景中,这里应该通过Ajax请求上服务端去验证
if("lisi" == this.username && "1234" == this.password){
// sessionStorage中存储的都是字符串值,
// 因此这里实际存储的将是字符串"true"
sessionStorage.setItem("isAuth", true);
this.info = "";
//如果存在查询参数
if(this.$route.query.redirect){
let redirect = this.$route.query.redirect;
//跳转至进入登录页前的路由
this.$router.replace(redirect);
}else{
// 否则跳转至首页
this.$router.replace('/');
}
}else{
sessionStorage.setItem("isAuth", false);
this.username = "";
this.password = "";
this.info = "用户名或密码错误";
}
}
}
}
</script>
修改路由配置文件index.js。这里只显示新增代码
...
import Login from '@/components/Login'
...
// 将VueRouter实例作为模块的默认导出
const router = new VueRouter({
mode: 'history',
routes: [
...
{
path: '/login',
name: 'login',
component: Login,
meta: {
title: '登录'
}
}
]
});
router.beforeEach((to, from, next) => {
// 判断目标路由是否是/login,如果是,直接调用next()方法
if(to.path == '/login'){
next();
}else{
// 否则判断用户是否已经登录,注意这里是字符串判断
if(sessionStorage.isAuth === "true"){
next();
}
// 如果用户访问的是受保护的资源,且没有登录,则跳转到登录页面,
// 并将当前路由的完整路径作为查询参数传给Login组件,以便登录成功后返回先前的页面
else{
next({
path: '/login',
query: {redirect: to.fullPath}
});
}
}
})
export default router;
要注意的是,代码中的 对路由是否是login的判断不能缺少,否则会导致死循环。例如:初次访问news,此时用户还没有登录,条件判断为false,跳转到login,然后又执行全局前置守卫,条件判断依然为false,再次跳转到login,最后导致栈溢出。
为了方便访问登录页面,可以在app.vue中新增一个登录的导航链接。
<router-link :to="{ name: 'login'}">登录</router-link>
完成上述修改后,运行项目。出现登录页面后,输入正确的用户名(lisi)和密码(1234),看看路由的跳转,之后输入错误的用户名和密码,再看看路由的跳转。
1.5 案例:页面标题
在单页面应用程序中,实际只有一个页面,因此在页面跳转时,标题不会发生改变。
在定义路由时,在routes配置中的每个路由对象(也称为路由记录)都可以使用一个meta字段,来为路由对象提供一个元数据信息。我们可以为每一个组件在他的路由记录里添加meta字段,在该字段中设置页面的标题,然后在全局后置钩子中设置目标路由页面的标题。全局后置钩子是在导航确认后,DOM更新前调用,因此在这个钩子中设置页面标题是比较合适的。
修改index.js中的代码
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/news',
name: 'news',
component: News,
meta: {
title: '新闻'
}
},
....
//下面类似,为每一个路由对象添加一个meta字段。
]
})
...
router.afterEach((to, from) => {
document.title = to.meta.title;
})
meta字段也可以用于对有限资源的保护,在需要保护的路由对象中添加一个需要验证属性,然后在全局前置守卫中进行判断,如果访问的是受保护的资源,继续判断用户是否已经登录,如果没有,则跳转到登录页面。
{
path: '/videos',
name: 'videos',
component: Videos,
meta: {
title: '视频',
requiresAuth: true
}
}
在全局前置守卫中进行判断。
router.beforeEach((to, from, next) => {
// 判断该路由是否需要登录权限
// 路由对象的matched属性是一个数组,包含了当前路由的所有嵌套路径片段的路由记录。
if (to.matched.some(record => record.meta.requiresAuth))
{
// 路由需要验证,判断用户是否已经登录
if(sessionStorage.isAuth === "true"){
next();
}
else{
next({
path: '/login',
query: {redirect: to.fullPath}
});
}
}else{
next();
}
})
1.6 matched
这里说明一下为什么要使用遍历to.matched数组判断meta的requiresAuth字段,而不直接使用to.meta.requiresAuth来判断。
我们需要知道的前提是以下两点:
- vue路由匹配时会同时匹配满足情况的所有路由,即如果路由是‘/cinema/plan’的话,‘/cinema’也会触发。
- 另外如果较高等级的路由需要登录控制的话,它所有的嵌套路由基本也需要登录控制。
先来看一个嵌套路由的例子
routes:
[
{
path: '/cinema',
redirect: '/page/cinema',
component: BlankLayout,
meta: { title: '影院' , requiresAuth: true}
children: [
{
path: '/cinema/plan',
name: 'cinemaPlan',
component: () => import('./views/cinema/Plan'),
meta: { title: '影院排期' }
},
{
path: '/cinema/cinemaDetail',
name: 'cinemaDetail',
component: () => import('./views/cinema/CinemaDetail'),
meta: { title: '影院详情' }
}
]
}
]
假设两种情况:
(1)cinema具有登录控制,而cinemaPlan 没有。如果用户正常点击路由跳转的话,它必然是先进一级路由,再去二级路由,一级路由实现登录控制,利用to.meta是能够满足的,注意这里是用户正常点击,但是假如有用户直接改变url地址的话去访问cinemaPlan的话,则需要给cinemaPlan路由添加requiresAuth字段,同理也需要给cinemaDetail添加字段,如果路由比较多的话,就会很麻烦。
(2)cinema没有登录控制,而cinemaPlan有。这种情况确实不怕用户直接改变url访问二级路由了,但是同样如果过多二级路由,也是需要设置许多requiresAuth。
所以,为了方便,直接遍历to.matched数组,该数组中保存着匹配到的所有路由信息。就该例而言,访问cinema时,matched数组长度为1,访问cinemaPlan时,matched数组长度为2,即保存着‘/cinema’以及‘/cinema/plan’。其实啰嗦了这么多,直接使用to.meta判断字段也可以,就是需要给所有需要控制的路由添加requiresAuth。而to.matched则只需要给较高一级的路由添加requiresAuth即可,其下的所有子路由不必添加。
2.路由独享守卫
路由独享守卫是在routes配置的路由对象中直接定义的beforeEnter守卫。
const router = new VueRouter({
routes: [
{
path: '/news',
name: 'news',
component: News,
meta: {
title: '新闻'
},
beforeEnter:(to,from,next)=>{
//....
}
},
....
//下面类似,为每一个路由对象添加一个meta字段。
]
})
beforeEnter守卫只在该组件上生效,在全局前置守卫调用之后,在进入路由组件之前调用。
3.组件内守卫
共有三个组件内守卫:beforeRouteEnter、beforeRouterUpdate和beforeRouteLeave。
//
const book = {
template:"...",
beforeRouteEnter (to, from, next) {
//在渲染该组件的路由被确认之前调用
//不能通过this来访问组件实例,因为在守卫执行前,组件实例还没有被创建
},
beforeRouteUpdate (to, from, next) {
//在渲染该组件的路由改变,但是该组件被复用时调用
//例如,对于一个带有动态参数的路由 /foo/:id,在/foo/1和/foo/2之间跳转的时候
//相同的foo组件实例将会被复用,而这个守卫就会在这种情况下被调用。
//可以访问组件实例的this
},
beforeRouteLeave (to, from, next) {
//导航即将离开该组件的路由时调用
//可以访问组件实例的this
}
}
beforeRouterEnter守卫不能访问this,因为在守卫是在导航确认前被调用,这时新进入的组件实例还没有被创建。
不过beforeRouteEnter有一个特权,就是它的next函数支持回调,而其他的守卫则不行。可以把组件实例作为回调方式的参数,在导航被确认后执行回调,而这个时候,组件实例已经创建完成。利用这个特性,可以将created钩子用beforeRoute守卫来替换。
<template>
<div>
<p>图书ID:{{ book.id }}</p>
<p>书名:{{ book.title }}</p>
<p>说明:{{ book.desc }}</p>
</div>
</template>
<script>
import Books from '@/assets/books'
export default {
data(){
return {
book: {}
}
},
methods: {
setBook(book){
this.book = book;
}
},
/* created(){
this.book = Books.find((item) => item.id == this.$route.params.id);
}, */
beforeRouteEnter (to, from, next) {
let book = Books.find((item) => item.id == to.params.id);
next(vm => vm.setBook(book));
},
//替换监听
beforeRouteUpdate (to, from, next) {
this.book = null;
this.book = Books.find((item) => item.id == to.params.id);
next();
}
}
</script>
assets目录下的books代码如下:
export default [
{id: 1, title: 'Vue.js无难事', desc: '前端框架经典图书'},
{id: 2, title: 'VC++深入详解', desc: '畅销10多年的图书'},
{id: 3, title: 'Servlet/JSP深入详解', desc: '经典JSP图书'}
]
beforeRouteLeave守卫通常用来防止用户在还未保存修改前突然离开,可以通过next(false)来取消导航。
beforeRouteLeave(to,from,next){
const answer = confirm('真的要离开吗?您还未保存已修改的内容!')
if(answer){
next()
}else{
next(false);
}
}