11. 导航守卫router.beforeEach

1,657 阅读1分钟

1. 路由改变过程中的导航守卫

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

正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。路由导航过程中的导航守卫有以下几种:全局的, 单个路由独享的, 或者组件级的

  • 记住参数或查询的改变并不会触发进入/离开的导航守卫。此时可以观察 $route 对象,或使用 beforeRouteUpdate 的组件内守卫。

2. 全局前置守卫router.beforeEach,路由跳转前触发

可以使用 router.beforeEach 注册一个全局前置守卫:

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
    console.log("to", to);
    console.log("from", from);
    console.log("next", next);
    next();
})

image.png 当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

每个守卫方法接收三个参数:

  • to: Route: 即将要进入的目标 路由对象

  • from: Route: 当前导航正要离开的路由

  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

    • next() : 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false) : 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next('/') 或者 next({ path: '/' }) : 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。
    • next(error) : 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到 /login 的示例:

// BAD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  // 如果用户未能验证身份,则 `next` 会被调用两次
  next()
})
// GOOD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  else next()
})


Non-nested routes must include a leading slash character. Fix the following routes的中文意思是(非嵌套路由必须包含前导斜杠字符)

image.png

image.png

router.beforeEach((to, from, next) => {
        console.log(to.path);
        if (to.path !== "/Login" && !isAuthenticated) next({ path: "/Login" });
        // 如果用户未能验证身份,则 `next` 会被调用两次
        else next();
      });

3. 全局解析守卫router.beforeResolve,组件解析后触发

可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,但是 router.beforeResolve在所有组件内守卫和异步路由组件被解析之后,在路由被确认之前,解析守卫被调用。

router.beforeResolve((to, from, next) => {
        console.log("再触发beforeResolve");
        console.log(to.path);
        console.log(from.path);
        next();
      });

image.png

4. 全局后置钩子router.afterEach, 在VueRouter实例router上定义

也可以注册全局后置钩子,然而和守卫不同的是,钩子router.afterEach不会接受 next 函数,也不会改变导航本身:

router.afterEach((to, from) => {
        console.log("接着触发afterEach");
        console.log(to.path);
        console.log(from.path);
      });

image.png

5. 单个路由独享的守卫beforeEnter, 在路由配置上定义

可以在路由配置routes上直接定义 beforeEnter 守卫:

const router = new VueRouter({
  routes: [
   {
        path: "/Login",
        component: Login,
        beforeEnter: (to, from, next) => {
          console.log("此时触发beforeEnter");
          console.log("to:", to.path);
          console.log("from:", from.path);
          next();
        }
  }]
})

image.png 单个路由守卫beforeEnter与全局前置守卫router.beforeEach的方法参数是一样的。

6. 组件内的守卫beforeRouteEnter, 在路由组件内定义

最后,可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate 
  • beforeRouteLeave
const Foo = {
  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,因为守卫在路由导航确认前被调用,因此即将登场的新组件还没被创建。

不过,我们可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}
  • 离开守卫 beforeRouteLeave 通常用来禁止用户在还未保存修改前突然离开。该路由导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

image.png

<!DOCTYPE html>
<html>
  <head>
    <title>vue-router</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.8/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-router@3.1.3/dist/vue-router.js"></script>
  </head>
  <body>
    <div id="app">
      <router-link to="/search?q=vue">search</router-link>
      <router-link to="/Login">login</router-link>
      <router-view></router-view>
    </div>
    <script>
      const SearchUser = {
        props: ["queryQ"],
        template: `<div class="SearchUser">
                      <h2>SearchUser {{ queryQ }}</h2>
                    </div>`
      };
      const Login = {
        props: ["queryQ"],
        template: `<div class="Login">
                      <h2>Login </h2>
                    </div>`,
        beforeRouteEnter(to, from, next) {
          console.log("触发组件守卫beforeRouteEnter");

          next(vm => {
            // 通过 `vm` 访问组件实例
            console.log("beforeRouteEnter回调访问组件实例");
            console.log("vm:", vm);
          });
        },
        beforeRouteUpdate(to, from, next) {
          // just use `this`
          console.log("触发组件守卫beforeRouteUpdate");
          console.log("this", this);
          next();
        },
        beforeRouteLeave(to, from, next) {
          console.log("触发组件守卫beforeRouteLeave");
          const answer = window.confirm(
            "Do you really want to leave? you have unsaved changes!"
          );
          if (answer) {
            next();
          } else {
            next(false);
          }
        }
      };
      const router = new VueRouter({
        routes: [
          {
            path: "/search",
            component: SearchUser,
            props: route => ({ queryQ: route.query.q })
          },
          {
            path: "/Login",
            component: Login,
            beforeEnter: (to, from, next) => {
              console.log("此时触发beforeEnter");
              console.log("to:", to.path);
              console.log("from:", from.path);
              next();
            }
          }
        ]
      });
      const isAuthenticated = false;
      router.beforeEach((to, from, next) => {
        console.log("先触发beforeEach");
        console.log("to:", to.path);
        console.log("from:", from.path);
        next();
      });
      router.beforeResolve((to, from, next) => {
        console.log("再触发beforeResolve");
        console.log("to:", to.path);
        console.log("from:", from.path);
        next();
      });
      router.afterEach((to, from) => {
        console.log("接着触发afterEach");
        console.log("to:", to.path);
        console.log("from:", from.path);
      });
      const app = new Vue({
        router,
        el: "#app"
      }).$mount("#app");
    </script>
  </body>
</html>

7. 完整的路由导航解析流程

  1. 路由导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 。
  9. 路由导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

image.png

graph TD
 路由导航被触发-->在失活的组件里调用beforeRouteLeave守卫 --> 调用全局的beforeEach守卫 --> 在路由配置里调用beforeEnter -->  解析异步路由组件 --> 在被激活的组件里调用beforeRouteEnter --> 调用全局的beforeResolve守卫 -->路由导航被确认-->调用全局的afterEach钩子-->触发DOM更新 -->调用beforeRouteEnter组件守卫中传给next的回调函数