# 路由守卫
『导航』表示路由正在发生改变。
vue-router 提供了能让你在导航发生之前运行某些代码的功能,并且遵照你的意愿去取消导航,或将用户导航到其它地方。
路由守卫有 3 种:
全局路由守卫;
路由独享守卫;
组件路由守卫。
# 1. 全局路由守卫
全局路由守卫最常见的使用场景就是『登录』认证和『权限』认证。
# 基本使用
假设你想限制未登录的用户访问某 URL,你甚至都已经准备好了一个方法(例如,名为 userAuthenticated())可以判断的当前用户是否已登录。该方法根据当前用户的登录情况『有时』返回 true,『有时』会返回 false 。
// 为简化逻辑,我们使用生成随机数来代替当前用户有可能登录,有可能没有登录。
function userAuthenticated() {
const randomNum = Math.round( Math.random() * 100 );
console.info(randomNum);
return randomNum % 2 === 0;
}
那么,现在你如何将 URL 和这个方法组合起来实现:『有时』允许用户访当前它所期望访问的 URL,『有时』则不允许其访问?
你可以为路由器(router)添加一个 router.beforeEach() 守卫:
const router = new VueRouter({ ... });
router.beforeEach((to, from, next) => {
// ...
});
该守卫被传入 3 各参数:to 、from 以及 next :
- Route to
- 路由对象,即将要进入的目标
- Route from
- 路由对象,当前导航正要离开的路由
- Function next
- 回调函数,通过 next 回调函数你可以让 vue-router 去处理导航、取消导航、重定向到其它地方或执行其它操作。
- 一定要调用 next 方法。因为直到调用 next 方法之前,路由行为的状态将一致处于等待状态,永远不会被解析(resolved)。
例如:
router.beforeEach((to, from, next) => {
if (to.path.startsWith("/acount") && !userAuthenticated())
next('/login'); // 重定向到 /login
else
next(); // 放行
});
next() 方法的几种常见形式:
next()
进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed:已确认。
next('/xxx')
跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
next(false)
中断当前的导航。并且强迫用户留在这里(from)。这个时候如果,你去手动修改浏览器的 URL 改变了,或者点击浏览器后退按钮,你会发现无效,你还在这里(from)。
# 利用路由元信息
在守卫中一个个去检查路径会让程序变得冗长不优雅,特别是当你维护一个拥有大量路由的系统时。此时,有一个很有用的特性可以被利用:路由元信息(route meta fields)。
你可以在路由上添加一个 meta 属性,并在路由守卫种获得到它。例如:
在 /account 路由上自定义一个 requiresAuth 属性,然后在路由守卫中获取该属性的值,并以此为依据决定是否要进行登录认证。
const router = new VueRouter({
rooutes: [
{
path: '/account',
component: AccountPage,
meta: {
requiresAuth: true // 看这里,自定义的一个 boolean 属性,属性名任意。
}
},
{ path: '...', ..., meta: { requiresAuth: true } }
{ path: '...', ..., meta: { requiresAuth: true } }
{ path: '...', ..., meta: { requiresAuth: false } }
{ path: '...', ..., meta: { requiresAuth: false } }
]
});
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !userAuthenticated()) {
next('/login'); // 重定向 login 页面
} else {
next(); // 放行
}
});
利用 meta ,上述代码实现了批量设置,而无需每个 URL 设置一次。
# 嵌套路由的特殊处理
当遇到嵌套路由的时候,meta 的设置还有简化的空间。例如,原版是这样:
routes: [
{
path: '/account', ...
children: [
{ path: '/account/xxx', ... meta: { requiresAuth: true } },
{ path: '/account/yyy', ... meta: { requiresAuth: true } },
{ path: '/account/zzz', ... meta: { requiresAuth: true } },
]
},
{ ... },
{ ... },
{ ... }
]
逻辑上,由于 /account 的子路由都需要进行登录校验,那么一个很自然的想法就是将,requiresAuth 加在父路由上,而没有必要每个子路由都加。这样配置将变为:
routes: [
{
// 父路由
path: '/account', ..., meta: { requiresAuth: true },
// 子路由
children: [
{ path: '/account/xxx', ... },
{ path: '/account/yyy', ... },
{ path: '/account/zzz', ... },
]
},
{ ... },
{ ... },
{ ... }
]
上述的优化思路是正确的。但是,如果仍用前例的路由守卫代码,则达不到预期效果:
router.beforeEach((to, from, next) => {
// 此时 to.meta.requiresAuth 有问题
if (to.meta.requiresAuth && !userAuthenticated())
next('/login');
else
next();
});
原因在于,当你要路由至 /account/xxx 、 /account/yyy 或 /account/zzz 时,to.meta.requiresAuth 取到的是子路由上的 requiresAuth,而这三个子路由的配置上是没有 requiresAuth 的!结果就成了:无需校验。
这种情况下需要使用 to.matched 的方式来曲线救国。
to 的 matched 属性是一个数组,逻辑上,它记录的是目标路由的 层级 数组。例如:
目标路由 | matched 数组长度 | 内容 |
---|---|---|
/account | 1 | /account |
/account/xxx | 2 | /account /account/xxx |
/account/xxx/yyy | 3 | /account /account/xxx /account/xxx/yyy |
/account/xxx/yyy/zzz | 4 | /account /account/xxx /account/xxx/yyy /account/xxx/yyy/zzz |
通过 to.matched 数组,你总能找到目标路由的父路由,而它的父路由上,有你所设置的 requiresAuth:
router.beforeEach((to, from, next) => {
// 查看目标路由及其父路由上有没有 requiresAuth
const requiresAuth = to.matched.some((record) => {
return record.meta.requiresAuth;
});
if (requiresAuth && !userAuthenticated()) {
next('/login');
} else {
next();
}
});
# 全局后置路由守卫(了解、自学)
全局路由除了前置路由守卫,还有后置路由守卫。
然而和前置路由守卫不同的是,这些后置路由不会接受 next 函数,即它们不会也无法改变本次导航本身(毕竟它们是后置守卫,当它们被触发时,导航已经完成了):
router.afterEach((to, from) => {
// ...
})
# 2. 路由独享守卫
路由独享守卫和全局守卫功能上是一样的。只不过全局守卫对每个路由有效,而路由独享守卫只针对『当前路由』有效。
你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/account',
component: AccountPage,
beforeEnter: (to, from, next) => {
if (!authenticated()) {
next('/login');
}
else {
next();
}
}
}
]
})
这些守卫与全局前置守卫的方法参数是一样的。
# 3. 组件内的守卫
组件内部守卫是指你在定义组件的时候(.vue 文件中),指定路由守卫。
能使用的守卫有 3 个:
beforeRouteEnter(等效于 beforeEach)
beforeRouteUpdate
beforeRouteLeave(等效于 afterEach)
# beforeRouterEnter 的 this 问题
顾名思义,beforeRouterEnter 是在进入当前地址,渲染本组件『之前』被调用的,那么此时,当前的组件对象自然还没有创建好!
因此,beforeRouterEnter 中『不能、不能、不能』获取组件实例 this 。
# beforeRouteEnter 的触发问题
beforeRouteEnter 只有在你彻底离开(触发了 beforeRouteLeave)组件/页面之后,再次进来时才会触发。一般情况下,好像并没有太大的问题,但是一旦是遇到动态路由的情况,就并非你所想。例如,/students/1
、/students/2
和 /students/3
在你看来是 3 个 URL,但是它们使用的是同一个组件!
因此,你在它们三个之间来回“跳转”时,你会发现 beforeRouteEnter 根本就没有触发!不光是 beforeRouteEnter,beforeRouteLeave 也没有触发。
这是因为,你根本就没有离开这个页面,只是在“切换”页面上的数据而已。
# beforeRouteUpdate 的使用场景
对于上述(动态路由)的情况,你需要使用的是 beforeRouteUpdate 路由守卫,这就是它的最常用场景。
不过,需要注意的是,你第一次“进来”的时候,触发的还是 beforeRouteEnter ,即它们的触发次数分别是 1 次和 N-1 次。
为了在同一个页面上,显示不同的数据,你需要在 beforeRouterUpdate 中去请求新的数据。
例如:
<template>
<div>
<div v-if="state === 'loading'">
Loading user ...
</div>
<div>
<h1>User: {{ userInfo.name }}</h1>
... etc ...
</div>
</div>
</template>
<script>
export default {
props: ['id'],
data: () => ({
state: 'loading',
userInfo: {
name: undefined
}
}),
mounted() {
console.log('mounted()');
this.init(this.id);
},
beforeRouteUpdate(to, from, next) {
console.log('beforeRouteUpdated()');
this.state = 'loading';
this.init(to.params.id);
next();
},
methods: {
init(id) {
console.log('执行 ajax 请求,去获取 ' + id + ' 的信息。并更新页面数据');
}
}
}
</script>
# 4. 完整的导航解析流程
# | 说明 |
---|---|
1 | 导航被触发。 |
2 | 在失活的组件里调用离开守卫。 |
3 | 调用全局的 beforeEach 守卫。 |
4 | 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。 |
5 | 在路由配置里调用 beforeEnter。 |
6 | 解析异步路由组件。 |
7 | 在被激活的组件里调用 beforeRouteEnter。 |
8 | 调用全局的 beforeResolve 守卫 (2.5+)。 |
9 | 导航被确认。 |
10 | 调用全局的 afterEach 钩子。 |
11 | 触发 DOM 更新。 |
12 | 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。 |
# 5. 路由的顺序问题
有两套路由设置:
方案一:
routes: [ { path: '/user/:id', component: UserPage }, { path: '/user/me', component: MePage } ]
方案二:
routes: [ { path: '/user/me', component: MePage }, { path: '/user/:id', component: UserPage } ]
上述两个方案区别并不大,都是定义了两个路由,一个是让当前用户访问自己的页面(/user/me
),另一个是用于访问其它用户的页面(例如,/user/9527
)。
两种方案的区别仅仅是两个路由的先后顺序的不同而已。
但是,方案一有一个问题:MePage 组件/页面永远不会被展示。因为,当你输入 /user/me 时,先匹配到的是 /user/:id ,此时,URI 中的 me 会被当作参数 id 的值!
方案二则不会有这个问题。
所以,当安排路由的先后顺序时,『精确的路由在前,含有统配含义的路由在后』。
# 6. 404 页面
可以利用 vue-router 的路由的『顺序搜索』规则与通配符 *
匹配的特点,来渲染一个显示错误页面。例如:
routes: [
... 其它路由 ...
{ path: '*', component: PageNotFound }
]
当其它路由都匹配不到时,就会显示 PageNotFound 组件。
如果想让子路由的错误页面也能在父组件中显示,则需要在子路由中添加该通配符路由:
routes: [
... 其它路由 ...,
{
path: '/settings', component: SettingsPage, children: [
{ path: 'profile', component: SettingProfilePage },
{ path: '*', component: PageNotFound }
]},
{ path: '*', component: PageNotFound }
]