前端知库

Vue Router 有哪些导航守卫?完整的导航流程是怎样的?

导航守卫是路由跳转过程中的钩子,分为全局守卫(beforeEach、beforeResolve、afterEach)、路由独享的 beforeEnter 和组件内守卫。Vue Router 4 推荐用返回值控制跳转:返回 false 取消,返回路由地址重定向,不返回或返回 true 放行。

参考答案

简答

用户从列表页点进详情页时,路由不是立刻切过去的,中间会经过一串钩子,每个钩子都能决定放行、取消还是改去别的页面,这些钩子就叫导航守卫。最常见的用法是在 router.beforeEach 里检查登录状态,没登录就跳到登录页。

守卫按作用范围分三类。全局守卫注册在 router 上,有 beforeEach、beforeResolve 和 afterEach;路由独享守卫是写在路由配置里的 beforeEnter;组件内守卫写在组件里,有 beforeRouteEnter、beforeRouteUpdate 和 beforeRouteLeave。afterEach 在导航确认后才执行,不能再改变导航,适合埋点和改页面标题。

完整顺序是:先调用要离开的组件的 beforeRouteLeave,再调用全局 beforeEach,然后是复用组件的 beforeRouteUpdate、路由配置的 beforeEnter,接着解析异步路由组件,调用要进入的组件的 beforeRouteEnter、全局 beforeResolve,导航确认后调用 afterEach,最后更新 DOM。

要注意写法上的版本差别。Vue Router 3 的守卫必须调用第三个参数 next() 才会继续;Vue Router 4 推荐直接返回值,返回 false 取消,返回一个路由地址就重定向,不返回或返回 true 就放行。next 在 4 里仍然支持,但每次经过守卫必须恰好调用一次,漏调或多调都会出问题。

用 beforeEach 做登录检查

下面是 Vue Router 4 的写法,需要登录的路由在 meta 里标记 requiresAuth:

JavaScript
router.beforeEach(async (to, from) => {
  if (to.meta.requiresAuth && !isAuthenticated() && to.name !== 'Login') {
    return { name: 'Login', query: { redirect: to.fullPath } };
  }
});

没有登录时,守卫返回登录页的地址,当前导航就被丢弃,换成一次去登录页的新导航。query.redirect 记下了原本想去的页面,登录成功后可以跳回来。

为什么要判断 to.name !== 'Login'?因为去登录页本身也是一次导航,也会经过 beforeEach。不加这个判断,跳到登录页时又会被重定向到登录页,形成无限重定向。

守卫可以是 async 函数,比如先 await 拉取用户信息再判断。Vue Router 会等它完成,导航在此期间处于等待状态。

Vue Router 3 的 next 写法为什么容易出错

Vue Router 3 中,守卫必须调用 next 才能继续:

JavaScript
// Vue Router 3 中有问题的写法
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated()) next({ name: 'Login' });
  next();
});

没登录时,这段代码先调用了 next({ name: 'Login' }),接着又执行到了 next(),同一次守卫里调用了两次。正确写法是用 else 把两条路径分开,保证每次只调一次。反过来,如果某个分支忘了调用 next,导航会一直卡住不动。

正是因为这类错误太常见,Vue Router 4 改成了用返回值表达结果,next 只是为了兼容保留下来。

组件内守卫各自什么时候用

beforeRouteLeave 在离开当前页面前调用,常用来拦截未保存的表单:

JavaScript
beforeRouteLeave() {
  if (this.hasUnsavedChanges && !window.confirm('还有未保存的修改,确定离开吗?')) {
    return false;
  }
}

beforeRouteUpdate 用在路由变了、组件却被复用的情况。比如从 /user/1 跳到 /user/2,两个地址用的是同一个 User 组件,组件不会重新创建,created、mounted 也不会再执行。这时要在 beforeRouteUpdate 里根据新的 id 重新拉数据,或者用 watch 侦听路由参数。

beforeRouteEnter 比较特殊,它在导航确认之前调用,这时组件实例还没有创建,所以不能用 this。如果确实要访问实例,可以给 next 传一个回调,等实例创建好以后执行:next((vm) => { ... })。这是唯一支持给 next 传回调的守卫。

组合式 API 里没有 beforeRouteEnter 的对应写法,另外两个可以用 onBeforeRouteLeave 和 onBeforeRouteUpdate。

常见追问

权限控制只靠路由守卫够吗?

不够。路由守卫只是在前端拦住页面跳转,用户可以直接调接口,也可以改前端代码绕过去。真正的权限校验必须放在服务端,前端守卫只负责体验。完整的分层设计见《前端权限控制怎样设计路由、按钮和接口三层?》。

beforeEach 和 beforeResolve 有什么区别?

beforeResolve 在所有组件内守卫和异步路由组件都解析完之后、导航确认之前调用,比 beforeEach 晚。需要在确定要进入页面后再做的检查,比如申请摄像头权限,适合放在 beforeResolve,这样不会因为后面的守卫取消了导航而白做。

动态添加的路由,守卫还会生效吗?

会。全局守卫对所有导航都生效,包括用 router.addRoute 动态添加的路由。常见做法是登录后拉取权限,用 addRoute 加上有权限的路由,再重新导航一次到目标地址,让新路由生效。