# 路由守卫

导航』表示路由正在发生改变。

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 各参数:tofrom 以及 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 的方式来曲线救国。

tomatched 属性是一个数组,逻辑上,它记录的是目标路由的 层级 数组。例如:

目标路由 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 }
]