解释 Vue Router 中的导航守卫(Navigation Guards),包括全局守卫、路由独享守卫和组件内守卫,并说明其执行顺序。

Vue Router 导航守卫:路由界的保安大队

大家好,我是你们今天的导游,不对,是讲师,带大家一起探秘 Vue Router 里的导航守卫。想象一下,你的网站是一栋大楼,而路由就是楼里的一个个房间。导航守卫呢?就是守卫在各个入口的保安大队,他们负责检查访客的身份、权限,决定是否允许进入。

这群保安大队可不是吃素的,他们分为三个小队,各司其职,共同维护网站的安全和秩序。

一、保安总队:全局守卫

全局守卫就像保安总队,他们负责监控所有的路由跳转,任何进出大楼的人都得经过他们的盘查。全局守卫有三个主要成员:

  1. beforeEach:前置守卫,进入大楼前检查

    这是最常用的全局守卫,它会在每次路由跳转 之前 被调用。你可以用它来做一些通用的权限验证、页面统计等操作。

    const router = new VueRouter({ ... });
    
    router.beforeEach((to, from, next) => {
      // to: 即将要进入的目标 路由对象
      // from: 当前导航正要离开的路由对象
      // next: 调用该方法后,才能进入下一个钩子
    
      console.log('即将进入:', to.path);
    
      // 模拟权限验证,只有登录用户才能访问 /admin 页面
      if (to.path === '/admin' && !localStorage.getItem('token')) {
        console.log('没有权限,跳转到登录页');
        next('/login'); // 跳转到登录页
      } else {
        next(); // 放行,允许进入下一个钩子
      }
    });

    代码解释:

    • to 对象包含了目标路由的信息,比如 to.path 是目标路由的路径。
    • from 对象包含了当前路由的信息。
    • next() 是一个函数,必须调用它才能进入下一个钩子。
      • next():允许进入下一个钩子。
      • next(false):中断当前的导航。
      • next('/')next({ path: '/' }):跳转到指定的路由。
      • next(error):如果传入 Error 实例,则导航会被终止,且该错误会被传递给 router.onError() 注册过的回调。

    注意事项:

    • 一定要调用 next() 方法,否则路由跳转会被阻塞。
    • 如果需要进行异步操作,可以在 next() 中传入一个函数,该函数会在异步操作完成后被调用。
  2. beforeResolve:解析守卫,大楼信息确认

    beforeResolve 守卫和 beforeEach 类似,区别在于它是在所有组件内守卫和异步路由组件被解析之后,组件被实际渲染之前 调用。这确保了在导航被确认之前,所有的组件都已经准备就绪。

    router.beforeResolve((to, from, next) => {
      console.log('组件解析完毕,准备渲染:', to.path);
      next();
    });

    这个守卫通常用于在路由确认之前执行一些最后的数据获取或验证操作。

  3. afterEach:后置守卫,离开大楼后记录

    afterEach 守卫会在每次路由跳转 之后 被调用。它不会收到 next 函数,因为导航已经完成。通常用它来做一些页面统计、日志记录等操作。

    router.afterEach((to, from) => {
      console.log('成功进入:', to.path);
      // 记录页面访问量
      // ...
    });

    注意事项:

    • afterEach 守卫没有 next 函数,因此无法中断导航。
    • 它主要用于执行一些副作用操作,比如页面统计。

二、楼层主管:路由独享守卫

路由独享守卫就像楼层主管,他们只负责管理自己楼层的房间,只允许特定的人进入。

路由独享守卫直接定义在路由配置中,只有 beforeEnter 这一个成员。

const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    beforeEnter: (to, from, next) => {
      console.log('进入 Admin 页面前检查权限');
      if (!localStorage.getItem('token')) {
        console.log('没有权限,跳转到登录页');
        next('/login');
      } else {
        next();
      }
    },
  },
  {
    path: '/profile/:id',
    component: UserProfile,
    beforeEnter: (to, from, next) => {
        const userId = to.params.id;
        // 假设这里根据 userId 从服务器获取用户数据
        fetchUser(userId)
          .then(user => {
            if (user) {
              next(); // 允许进入
            } else {
              next('/404'); // 用户不存在,跳转到 404 页面
            }
          })
          .catch(error => {
            console.error("获取用户信息失败:", error);
            next('/error'); // 跳转到错误页面
          });
      }
  }
];

const router = new VueRouter({
  routes,
});

代码解释:

  • beforeEnter 守卫只会在进入 /admin 路由之前被调用。
  • 它可以访问 tofromnext 对象,和全局 beforeEach 守卫一样。

使用场景:

  • 只针对特定路由进行权限验证。
  • 在进入特定路由之前,预先加载一些数据。

三、房间管家:组件内守卫

组件内守卫就像房间管家,他们负责管理自己房间的进出,确保只有合适的客人才能入住。

组件内守卫定义在组件内部,有三个成员:

  1. beforeRouteEnter:进入房间前

    beforeRouteEnter 守卫在路由进入该组件 之前 被调用。但是,因为在导航确认前,组件实例还未被创建,所以 不能 直接访问 this

    export default {
      template: `<div>{{ message }}</div>`,
      data() {
        return {
          message: 'Hello from UserProfile',
        };
      },
      beforeRouteEnter(to, from, next) {
        console.log('进入 UserProfile 组件前');
    
        // 无法访问 `this`
        // console.log(this.message); // undefined
    
        // 可以通过 next 的回调函数访问组件实例
        next(vm => {
          console.log('成功进入 UserProfile 组件');
          console.log(vm.message); // 可以访问 `this`
        });
      },
    };

    代码解释:

    • beforeRouteEnter 无法直接访问 this,因为组件实例还未被创建。
    • 可以通过 next 的回调函数访问组件实例,回调函数会在组件实例创建之后被调用。

    使用场景:

    • 在组件创建之前,进行一些初始化操作。
    • 在组件创建之前,访问路由参数。
  2. beforeRouteUpdate:房间信息更新时

    beforeRouteUpdate 守卫在当前路由改变,但是该组件被复用时调用。例如,对于一个带有动态参数的路由 /users/:id,当用户从 /users/1 导航到 /users/2 时,UserProfile 组件会被复用,beforeRouteUpdate 守卫会被调用。

    可以访问组件实例 this

    export default {
      template: `<div>User ID: {{ userId }}</div>`,
      props: ['id'],
      watch: {
        id(newId, oldId) {
          console.log(`User ID changed from ${oldId} to ${newId}`);
          this.fetchUser(newId);
        }
      },
      beforeRouteUpdate (to, from, next) {
        console.log('UserProfile 组件更新');
        this.userId = to.params.id;
        next()
      },
      data() {
        return {
          userId: this.id
        };
      },
      mounted() {
        this.fetchUser(this.id);
      },
      methods: {
        fetchUser(userId) {
          // 从服务器获取用户信息
          console.log(`Fetching user data for ID: ${userId}`);
          // ...
        }
      }
    };

    代码解释:

    • beforeRouteUpdate 可以访问组件实例 this
    • 可以在这个守卫中更新组件的数据,或者执行一些其他的操作。

    使用场景:

    • 在组件复用时,更新组件的数据。
    • 在组件复用时,重新加载数据。
  3. beforeRouteLeave:离开房间前

    beforeRouteLeave 守卫在离开当前路由对应的组件 之前 被调用。可以访问组件实例 this

    export default {
      template: `<div>Are you sure you want to leave?</div>`,
      beforeRouteLeave(to, from, next) {
        console.log('离开 UserProfile 组件前');
        const answer = window.confirm('确定要离开吗?');
        if (answer) {
          next();
        } else {
          next(false);
        }
      },
    };

    代码解释:

    • beforeRouteLeave 可以访问组件实例 this
    • 可以在这个守卫中阻止用户离开当前路由。

    使用场景:

    • 在用户离开当前路由之前,弹出确认对话框。
    • 在用户离开当前路由之前,保存用户的数据。

四、保安大队的执行顺序:谁先谁后?

了解了各个保安小队的职责之后,我们再来看看他们的执行顺序,就像了解军队的指挥系统一样重要。

一个完整的导航过程,守卫的执行顺序如下:

  1. 全局 beforeEach 守卫:保安总队第一个出动,检查所有进出的人。
  2. 路由独享 beforeEnter 守卫:楼层主管开始检查,看看你是否有权限进入该楼层。
  3. 组件内 beforeRouteLeave 守卫:房间管家开始检查,看看你是否允许离开房间。
  4. 全局 beforeResolve 守卫:保安总队再次出动,确认大楼信息。
  5. 组件内 beforeRouteEnter 守卫:房间管家开始检查,看看你是否有权限进入房间 (回调函数在组件挂载后执行)。
  6. 组件内 beforeRouteUpdate 守卫: 如果路由改变但组件被复用,则执行。
  7. 全局 afterEach 守卫:保安总队最后出动,记录所有进出的人。

为了更清晰地展示,我们用一个表格来总结:

守卫类型 守卫名称 执行时机 是否可以访问 this 是否需要调用 next()
全局守卫 beforeEach 在每次路由跳转 之前
路由独享守卫 beforeEnter 在进入特定路由 之前
组件内守卫 beforeRouteLeave 在离开当前组件对应的路由 之前
全局守卫 beforeResolve 在所有组件内守卫和异步组件解析 之后
组件内守卫 beforeRouteEnter 在进入组件对应的路由 之前 (回调函数) 否 (回调函数中可以)
组件内守卫 beforeRouteUpdate 在当前路由改变,但是该组件被复用时
全局守卫 afterEach 在每次路由跳转 之后

举个例子:

假设我们有以下路由配置:

const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    beforeEnter: (to, from, next) => {
      console.log('2. 路由独享:进入 Admin 页面前');
      next();
    },
    beforeRouteEnter(to, from, next) {
        console.log('5. 组件内:进入 AdminPanel 组件前');
        next();
    },
    beforeRouteLeave(to, from, next) {
        console.log('3. 组件内:离开 AdminPanel 组件前');
        next();
    },
  },
  {
    path: '/home',
    component: HomePanel,
  },
];

const router = new VueRouter({
  routes,
});

router.beforeEach((to, from, next) => {
  console.log('1. 全局:即将进入:', to.path);
  next();
});

router.afterEach((to, from) => {
  console.log('6. 全局:成功进入:', to.path);
});

当我们从 /home 导航到 /admin 时,控制台的输出顺序将会是:

1. 全局:即将进入: /admin
2. 路由独享:进入 Admin 页面前
3. 组件内:进入 AdminPanel 组件前
4. 全局:成功进入: /admin

如果我们从 /admin 导航到 /home 时,控制台的输出顺序将会是:

1. 全局:即将进入: /home
2. 组件内:离开 AdminPanel 组件前
3. 全局:成功进入: /home

五、实战演练:一个完整的权限控制示例

现在,我们来用一个更完整的例子,演示如何使用导航守卫进行权限控制。

假设我们有一个需要登录才能访问的后台管理系统。

  1. 定义路由:

    const routes = [
      { path: '/login', component: Login },
      {
        path: '/admin',
        component: AdminPanel,
        meta: { requiresAuth: true }, // 标记该路由需要登录
      },
      { path: '*', component: NotFound }, // 404 页面
    ];
  2. 全局 beforeEach 守卫:

    router.beforeEach((to, from, next) => {
      const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
      const isLoggedIn = localStorage.getItem('token');
    
      if (requiresAuth && !isLoggedIn) {
        console.log('需要登录,跳转到登录页');
        next('/login');
      } else if (to.path === '/login' && isLoggedIn) {
        console.log('已经登录,跳转到首页');
        next('/');
      } else {
        next();
      }
    });

    代码解释:

    • to.matched 是一个数组,包含了当前路由匹配到的所有路由记录。
    • record.meta 是路由元信息,可以在路由配置中自定义。
    • requiresAuth 标记该路由是否需要登录。
    • isLoggedIn 检查用户是否已经登录。
    • 如果需要登录,但是用户没有登录,则跳转到登录页。
    • 如果用户已经登录,但是访问登录页,则跳转到首页。
  3. 登录组件:

    <template>
      <div>
        <h1>Login</h1>
        <button @click="login">Login</button>
      </div>
    </template>
    
    <script>
    export default {
      methods: {
        login() {
          // 模拟登录成功
          localStorage.setItem('token', 'fake_token');
          this.$router.push('/admin'); // 跳转到后台管理页面
        },
      },
    };
    </script>

    代码解释:

    • 点击登录按钮,模拟登录成功,设置 tokenlocalStorage
    • 跳转到后台管理页面。
  4. 退出登录:

    在后台管理组件中,添加一个退出登录的按钮:

    <template>
      <div>
        <h1>Admin Panel</h1>
        <button @click="logout">Logout</button>
      </div>
    </template>
    
    <script>
    export default {
      methods: {
        logout() {
          localStorage.removeItem('token');
          this.$router.push('/login'); // 跳转到登录页
        },
      },
    };
    </script>

    代码解释:

    • 点击退出登录按钮,清除 token
    • 跳转到登录页。

通过以上步骤,我们就实现了一个简单的权限控制系统。

六、总结

Vue Router 的导航守卫就像网站的保安大队,他们负责维护网站的安全和秩序。

  • 全局守卫 负责监控所有的路由跳转。
  • 路由独享守卫 负责管理特定路由的访问权限。
  • 组件内守卫 负责管理组件的生命周期和数据更新。

了解了这些守卫的职责和执行顺序,你就可以轻松地构建一个安全、高效的 Vue 应用。

希望今天的讲座对大家有所帮助! 记住,路由守卫是你的好帮手,用好它们,你的网站才能更安全、更流畅!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注