Vue 教程路由模块之路由守卫深度剖析(六)

Vue 框架路由模块之路由守卫深度剖析

一、引言

在现代单页面应用(SPA)的开发中,路由管理是至关重要的一环。Vue.js 作为一款流行的前端框架,其官方路由管理器 Vue Router 提供了强大而灵活的路由功能。其中,路由守卫(Route Guards)是 Vue Router 中一个非常重要的特性,它允许我们在路由切换的不同阶段执行特定的逻辑,比如进行权限验证、数据预加载、页面过渡效果控制等。通过合理使用路由守卫,我们可以增强应用的安全性、优化用户体验,并实现复杂的导航逻辑。

本文将深入探讨 Vue Router 中的路由守卫,从基本概念和使用场景出发,逐步深入到源码级别进行分析,详细解读每个路由守卫的实现原理和工作流程。通过对源码的剖析,我们能够更好地理解路由守卫的工作机制,从而在实际开发中更加灵活、高效地运用它们。

二、路由守卫概述

2.1 路由守卫的定义

路由守卫是 Vue Router 提供的一种机制,用于在路由切换的不同阶段执行自定义的逻辑。这些逻辑可以影响路由的跳转结果,例如允许或阻止路由跳转、重定向到其他路由等。路由守卫可以分为全局守卫、路由独享守卫和组件内守卫,它们分别在不同的作用域下生效。

2.2 路由守卫的作用

  • 权限验证:在用户访问某些受保护的路由时,检查用户的登录状态或权限,确保只有授权用户才能访问。
  • 数据预加载:在路由切换前,提前加载页面所需的数据,避免在页面渲染后再发起请求,提高用户体验。
  • 导航控制:根据不同的条件,决定是否允许用户进行路由切换,或者将用户重定向到其他页面。
  • 页面过渡效果控制:在路由切换前后执行特定的动画效果,增强用户体验。

2.3 路由守卫的分类

  • 全局守卫 :包括全局前置守卫(beforeEach)、全局解析守卫(beforeResolve)和全局后置守卫(afterEach),它们会在所有路由切换时生效。
  • 路由独享守卫 :通过在路由配置中定义 beforeEnter 守卫,只对该路由生效。
  • 组件内守卫 :在组件内部定义的守卫,包括 beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave,只对该组件生效。

三、全局路由守卫

3.1 全局前置守卫(beforeEach

3.1.1 基本使用

全局前置守卫是最常用的路由守卫之一,它会在每次路由切换前执行。以下是一个简单的示例:

javascript

javascript 复制代码
import Vue from 'vue';
import VueRouter from 'vue-router';

// 使用 Vue Router 插件
Vue.use(VueRouter);

// 定义路由组件
const Home = { template: '<div>Home Page</div>' };
const About = { template: '<div>About Page</div>' };

// 定义路由配置数组
const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/about',
    component: About
  }
];

// 创建路由实例
const router = new VueRouter({
  routes
});

// 注册全局前置守卫
router.beforeEach((to, from, next) => {
  // to: 即将要进入的目标路由对象
  // from: 当前导航正要离开的路由对象
  // next: 用于控制路由跳转的函数
  console.log('即将从', from.path, '导航到', to.path);
  // 允许路由跳转
  next(); 
});

// 创建 Vue 实例并挂载路由
new Vue({
  router
}).$mount('#app');

在上述代码中,我们通过 router.beforeEach 方法注册了一个全局前置守卫。每次路由切换前,都会打印出当前要离开的路由和即将进入的路由信息,然后调用 next() 函数允许路由跳转。

3.1.2 next 函数的使用

next 函数是全局前置守卫中非常重要的一个参数,它用于控制路由的跳转。next 函数有以下几种使用方式:

  • next() :允许路由跳转,继续执行后续的导航流程。

  • next(false) :取消当前的导航,如果浏览器的 URL 改变了(可能是用户手动修改了 URL),则会将 URL 重置到 from 路由对应的 URL。

  • next('/')next({ path: '/' }) :跳转到指定的路由,可用于重定向。

  • next(error) :如果传入一个 Error 实例,导航会被终止,并且该错误会被传递给 router.onError() 注册的回调函数。

以下是一个使用 next 函数进行权限验证的示例:

javascript

javascript 复制代码
router.beforeEach((to, from, next) => {
  // 假设 isAuthenticated 函数用于检查用户是否登录
  const isAuthenticated = () => {
    // 这里可以实现具体的登录状态检查逻辑
    return localStorage.getItem('token') !== null;
  };

  // 检查目标路由是否需要登录权限
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // 如果未登录,重定向到登录页面
    next({ path: '/login' });
  } else {
    // 允许路由跳转
    next();
  }
});

在上述代码中,我们通过检查 to.meta.requiresAuth 属性来判断目标路由是否需要登录权限。如果需要登录权限且用户未登录,则重定向到登录页面;否则,允许路由跳转。

3.1.3 源码分析

下面我们深入到 Vue Router 的源码中,看看全局前置守卫是如何实现的。

首先,在 VueRouter 构造函数中,会初始化全局前置守卫数组:

javascript

javascript 复制代码
export default class VueRouter {
  constructor(options = {}) {
    // 初始化全局前置守卫数组
    this.beforeHooks = []; 
    // 其他初始化操作...
    // 注册全局前置守卫
    options.beforeEach && this.beforeEach(options.beforeEach);
  }

  // 注册全局前置守卫的方法
  beforeEach(fn) {
    // 将守卫函数添加到全局前置守卫数组中
    this.beforeHooks.push(fn);
    return () => {
      // 返回一个取消注册的函数
      const i = this.beforeHooks.indexOf(fn);
      if (i > -1) this.beforeHooks.splice(i, 1);
    };
  }
}

在上述代码中,this.beforeHooks 是一个数组,用于存储所有注册的全局前置守卫函数。beforeEach 方法用于将传入的守卫函数添加到 this.beforeHooks 数组中,并返回一个取消注册的函数。

在路由切换时,会执行全局前置守卫。具体的执行逻辑在 transitionTo 方法中:

javascript

javascript 复制代码
class History {
  transitionTo(location, onComplete, onAbort) {
    const route = this.router.match(location, this.current);
    this.confirmTransition(route, () => {
      // 其他操作...
    }, err => {
      // 处理错误...
    });
  }

  confirmTransition(route, onComplete, onAbort) {
    const current = this.current;
    const abort = err => {
      // 处理导航终止的情况
      if (isError(err)) {
        if (this.router.onError) {
          this.router.onError(err);
        }
        if (onAbort) {
          onAbort(err);
        }
      } else if (onAbort) {
        onAbort();
      }
    };

    const resolveQueue = [
      // 全局前置守卫
      ...this.router.beforeHooks, 
      // 路由独享守卫
      ...this.getBeforeEnterHooks(route), 
      // 组件内守卫
      ...resolveAsyncComponents(route)
    ];

    const iterator = (hook, next) => {
      try {
        hook(route, current, (to) => {
          if (to === false) {
            // 取消导航
            abort();
          } else if (isError(to)) {
            // 导航出错
            abort(to);
          } else if (typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string'))) {
            // 重定向
            this.transitionTo(to, onComplete, abort);
          } else {
            // 继续执行下一个守卫
            next();
          }
        });
      } catch (e) {
        abort(e);
      }
    };

    // 依次执行守卫队列中的守卫
    runQueue(resolveQueue, iterator, () => {
      // 所有守卫执行完毕,继续后续操作
      this.updateRoute(route);
      onComplete && onComplete(route);
    });
  }
}

在上述代码中,confirmTransition 方法负责执行所有的路由守卫。resolveQueue 数组包含了全局前置守卫、路由独享守卫和组件内守卫。iterator 函数用于执行每个守卫,并根据守卫的返回值进行相应的处理。最后,通过 runQueue 函数依次执行守卫队列中的守卫。

3.2 全局解析守卫(beforeResolve

3.2.1 基本使用

全局解析守卫在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用。它的使用方式和全局前置守卫类似:

javascript

javascript 复制代码
router.beforeResolve((to, from, next) => {
  console.log('全局解析守卫:即将从', from.path, '导航到', to.path);
  next();
});

全局解析守卫通常用于在路由切换前进行最后的数据验证或处理。

3.2.2 源码分析

VueRouter 构造函数中,会初始化全局解析守卫数组:

javascript

javascript 复制代码
export default class VueRouter {
  constructor(options = {}) {
    // 初始化全局解析守卫数组
    this.resolveHooks = []; 
    // 其他初始化操作...
    // 注册全局解析守卫
    options.beforeResolve && this.beforeResolve(options.beforeResolve);
  }

  // 注册全局解析守卫的方法
  beforeResolve(fn) {
    // 将守卫函数添加到全局解析守卫数组中
    this.resolveHooks.push(fn);
    return () => {
      // 返回一个取消注册的函数
      const i = this.resolveHooks.indexOf(fn);
      if (i > -1) this.resolveHooks.splice(i, 1);
    };
  }
}

confirmTransition 方法中,全局解析守卫会在所有组件内守卫和异步路由组件被解析之后执行:

javascript

javascript 复制代码
class History {
  confirmTransition(route, onComplete, onAbort) {
    const current = this.current;
    const abort = err => {
      // 处理导航终止的情况
      if (isError(err)) {
        if (this.router.onError) {
          this.router.onError(err);
        }
        if (onAbort) {
          onAbort(err);
        }
      } else if (onAbort) {
        onAbort();
      }
    };

    const resolveQueue = [
      // 全局前置守卫
      ...this.router.beforeHooks, 
      // 路由独享守卫
      ...this.getBeforeEnterHooks(route), 
      // 组件内守卫
      ...resolveAsyncComponents(route),
      // 全局解析守卫
      ...this.router.resolveHooks 
    ];

    const iterator = (hook, next) => {
      try {
        hook(route, current, (to) => {
          if (to === false) {
            // 取消导航
            abort();
          } else if (isError(to)) {
            // 导航出错
            abort(to);
          } else if (typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string'))) {
            // 重定向
            this.transitionTo(to, onComplete, abort);
          } else {
            // 继续执行下一个守卫
            next();
          }
        });
      } catch (e) {
        abort(e);
      }
    };

    // 依次执行守卫队列中的守卫
    runQueue(resolveQueue, iterator, () => {
      // 所有守卫执行完毕,继续后续操作
      this.updateRoute(route);
      onComplete && onComplete(route);
    });
  }
}

在上述代码中,resolveQueue 数组包含了全局解析守卫,它会在所有组件内守卫和异步路由组件被解析之后执行。

3.3 全局后置守卫(afterEach

3.3.1 基本使用

全局后置守卫在路由切换完成后执行,它不接收 next 函数,因为它不会影响路由的跳转。以下是一个简单的示例:

javascript

javascript 复制代码
router.afterEach((to, from) => {
  console.log('全局后置守卫:已从', from.path, '导航到', to.path);
});

全局后置守卫通常用于记录路由切换日志、页面统计等操作。

3.3.2 源码分析

VueRouter 构造函数中,会初始化全局后置守卫数组:

javascript

javascript 复制代码
export default class VueRouter {
  constructor(options = {}) {
    // 初始化全局后置守卫数组
    this.afterHooks = []; 
    // 其他初始化操作...
    // 注册全局后置守卫
    options.afterEach && this.afterEach(options.afterEach);
  }

  // 注册全局后置守卫的方法
  afterEach(fn) {
    // 将守卫函数添加到全局后置守卫数组中
    this.afterHooks.push(fn);
    return () => {
      // 返回一个取消注册的函数
      const i = this.afterHooks.indexOf(fn);
      if (i > -1) this.afterHooks.splice(i, 1);
    };
  }
}

confirmTransition 方法中,全局后置守卫会在路由切换完成后执行:

javascript

javascript 复制代码
class History {
  confirmTransition(route, onComplete, onAbort) {
    const current = this.current;
    const abort = err => {
      // 处理导航终止的情况
      if (isError(err)) {
        if (this.router.onError) {
          this.router.onError(err);
        }
        if (onAbort) {
          onAbort(err);
        }
      } else if (onAbort) {
        onAbort();
      }
    };

    const resolveQueue = [
      // 全局前置守卫
      ...this.router.beforeHooks, 
      // 路由独享守卫
      ...this.getBeforeEnterHooks(route), 
      // 组件内守卫
      ...resolveAsyncComponents(route),
      // 全局解析守卫
      ...this.router.resolveHooks 
    ];

    const iterator = (hook, next) => {
      try {
        hook(route, current, (to) => {
          if (to === false) {
            // 取消导航
            abort();
          } else if (isError(to)) {
            // 导航出错
            abort(to);
          } else if (typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string'))) {
            // 重定向
            this.transitionTo(to, onComplete, abort);
          } else {
            // 继续执行下一个守卫
            next();
          }
        });
      } catch (e) {
        abort(e);
      }
    };

    // 依次执行守卫队列中的守卫
    runQueue(resolveQueue, iterator, () => {
      // 所有守卫执行完毕,继续后续操作
      this.updateRoute(route);
      onComplete && onComplete(route);
      // 执行全局后置守卫
      this.router.afterHooks.forEach(hook => {
        hook(route, current);
      });
    });
  }
}

在上述代码中,在所有守卫执行完毕且路由切换完成后,会依次执行全局后置守卫数组中的每个守卫函数。

四、路由独享守卫

4.1 路由独享守卫的基本使用

路由独享守卫是在单个路由配置中定义的守卫,只对该路由生效。可以使用 beforeEnter 来定义路由独享守卫。以下是一个示例:

javascript

javascript 复制代码
const routes = [
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    // 路由独享守卫
    beforeEnter: (to, from, next) => {
      // 检查用户是否有管理员权限
      const isAdmin = () => {
        // 这里可以实现具体的管理员权限检查逻辑
        return localStorage.getItem('role') === 'admin';
      };

      if (isAdmin()) {
        // 允许路由跳转
        next();
      } else {
        // 跳转到无权限页面
        next({ name: 'NoAccess' });
      }
    }
  }
];

在上述代码中,当用户访问 /admin 路由时,会先执行 beforeEnter 守卫,检查用户是否有管理员权限。如果有管理员权限,则允许路由跳转;否则,跳转到无权限页面。

4.2 源码分析

addRouteRecord 函数中,会将路由独享守卫存储在路由记录中:

javascript

javascript 复制代码
function addRouteRecord(pathList, routeMap, route, parent) {
  const path = parent ? normalizePath(route.path, parent) : route.path;
  const record = {
    path,
    name: route.name,
    component: route.component,
    parent,
    meta: route.meta || {},
    // 存储路由独享守卫
    beforeEnter: route.beforeEnter 
  };

  if (route.children) {
    route.children.forEach(child => {
      addRouteRecord(pathList, routeMap, child, record);
    });
  }

  if (!routeMap[path]) {
    pathList.push(path);
    routeMap[path] = record;
  }
}

confirmTransition 方法中,会获取并执行路由独享守卫:

javascript

javascript 复制代码
class History {
  confirmTransition(route, onComplete, onAbort) {
    const current = this.current;
    const abort = err => {
      // 处理导航终止的情况
      if (isError(err)) {
        if (this.router.onError) {
          this.router.onError(err);
        }
        if (onAbort) {
          onAbort(err);
        }
      } else if (onAbort) {
        onAbort();
      }
    };

    const resolveQueue = [
      // 全局前置守卫
      ...this.router.beforeHooks, 
      // 获取并添加路由独享守卫
      ...this.getBeforeEnterHooks(route), 
      // 组件内守卫
      ...resolveAsyncComponents(route),
      // 全局解析守卫
      ...this.router.resolveHooks 
    ];

    const iterator = (hook, next) => {
      try {
        hook(route, current, (to) => {
          if (to === false) {
            // 取消导航
            abort();
          } else if (isError(to)) {
            // 导航出错
            abort(to);
          } else if (typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string'))) {
            // 重定向
            this.transitionTo(to, onComplete, abort);
          } else {
            // 继续执行下一个守卫
            next();
          }
        });
      } catch (e) {
        abort(e);
      }
    };

    // 依次执行守卫队列中的守卫
    runQueue(resolveQueue, iterator, () => {
      // 所有守卫执行完毕,继续后续操作
      this.updateRoute(route);
      onComplete && onComplete(route);
      // 执行全局后置守卫
      this.router.afterHooks.forEach(hook => {
        hook(route, current);
      });
    });
  }

  // 获取路由独享守卫的方法
  getBeforeEnterHooks(route) {
    const hooks = [];
    route.matched.forEach(record => {
      if (record.beforeEnter) {
        hooks.push(record.beforeEnter);
      }
    });
    return hooks;
  }
}

在上述代码中,addRouteRecord 函数将路由独享守卫存储在路由记录中。getBeforeEnterHooks 方法用于获取匹配路由的所有路由独享守卫,并添加到 resolveQueue 数组中,在路由切换时执行。

五、组件内守卫

5.1 beforeRouteEnter

5.1.1 基本使用

beforeRouteEnter 是在路由进入组件前执行的守卫,它接收三个参数:tofromnext。由于此时组件实例还未创建,不能直接访问 this。可以通过 next 函数的回调来访问组件实例。以下是一个示例:

vue

javascript 复制代码
<template>
  <div>
    <!-- 组件内容 -->
  </div>
</template>

<script>
export default {
  name: 'User',
  beforeRouteEnter(to, from, next) {
    // 检查用户是否有访问权限
    const hasAccess = () => {
      // 这里可以实现具体的访问权限检查逻辑
      return localStorage.getItem('token') !== null;
    };

    if (hasAccess()) {
      // 允许路由跳转,并在组件实例创建后访问实例
      next(vm => {
        // 可以在这里访问组件实例 vm
        vm.initData(); 
      });
    } else {
      // 跳转到无权限页面
      next({ name: 'NoAccess' });
    }
  },
  methods: {
    initData() {
      // 初始化数据的方法
      console.log('初始化数据');
    }
  }
};
</script>

在上述代码中,beforeRouteEnter 守卫用于检查用户是否有访问权限。如果有访问权限,则允许路由跳转,并在组件实例创建后调用 initData 方法;否则,跳转到无权限页面。

5.1.2 源码分析

resolveAsyncComponents 函数中,会处理组件内守卫:

javascript

javascript 复制代码
function resolveAsyncComponents(route) {
  const guards = [];
  route.matched.forEach((record, i) => {
    const component = record.components.default;
    if (component) {
      const hooks = [];
      if (i > 0) {
        const prevRecord = route.matched[i - 1];
        if (prevRecord) {
          // 添加 beforeRouteUpdate 守卫
          component.beforeRouteUpdate && hooks.push(component.beforeRouteUpdate);
        }
      }
      if (i === route.matched.length - 1) {
        // 添加 beforeRouteEnter 守卫
        component.beforeRouteEnter && hooks.push(component.beforeRouteEnter);
      }
      guards.push(...hooks);
    }
  });
  return guards;
}

在上述代码中,resolveAsyncComponents 函数会遍历匹配的路由记录,获取每个组件的 beforeRouteEnter 守卫,并添加到 guards 数组中。在 confirmTransition 方法中,会将这些守卫添加到 resolveQueue 数组中执行。

5.2 beforeRouteUpdate

5.2.1 基本使用

beforeRouteUpdate 是在路由更新时执行的守卫,它接收三个参数:tofromnext。此时组件实例已经存在,可以直接访问 this。以下是一个示例:

vue

javascript 复制代码
<template>
  <div>
    <!-- 组件内容 -->
  </div>
</template>

<script>
export default {
  name: 'User',
  beforeRouteUpdate(to, from, next) {
    // 更新组件数据
    this.updateData(to.params.id); 
    // 允许路由跳转
    next(); 
  },
  methods: {
    updateData(id) {
      // 更新数据的方法
      console.log('更新数据,ID 为', id);
    }
  }
};
</script>

在上述代码中,当路由更新时,beforeRouteUpdate 守卫会调用 updateData 方法更新组件数据,然后允许路由跳转。

5.2.2 源码分析

resolveAsyncComponents 函数中,会处理 beforeRouteUpdate 守卫:

javascript

javascript 复制代码
function resolveAsyncComponents(route) {
  const guards = [];
  route.matched.forEach((record, i) => {
    const component = record.components.default;
    if (component) {
      const hooks = [];
      if (i > 0) {
        const prevRecord = route.matched[i - 1];
        if (prevRecord) {
          // 添加 beforeRouteUpdate 守卫
          component.beforeRouteUpdate && hooks.push(component.beforeRouteUpdate);
        }
      }
      if (i === route.matched.length - 1) {
        // 添加 beforeRouteEnter 守卫
        component.beforeRouteEnter && hooks.push(component.beforeRouteEnter);
      }
      guards.push(...hooks);
    }
  });
  return guards;
}

在上述代码中,resolveAsyncComponents 函数会遍历匹配的路由记录,获取每个组件的 beforeRouteUpdate 守卫,并添加到 guards 数组中。在 confirmTransition 方法中,会将这些守卫添加到 resolveQueue 数组中执行。

5.3 beforeRouteLeave

5.3.1 基本使用

beforeRouteLeave 是在路由离开组件前执行的守卫,它接收三个参数:tofromnext。常用于提示用户是否保存未保存的数据。以下是一个示例:

vue

javascript 复制代码
<template>
  <div>
    <!-- 组件内容 -->
  </div>
</template>

<script>
export default {
  name: 'User',
  beforeRouteLeave(to, from, next) {
    // 检查是否有未保存的数据
    const hasUnsavedData = () => {
      // 这里可以实现具体的未保存数据检查逻辑
      return this.formDataChanged;
    };

    if (hasUnsavedData()) {
      // 提示用户是否保存数据
      if (confirm('有未保存的数据,是否保存?')) {
        this.saveData();
      }
    }
    // 允许路由跳转
    next(); 
  },
  data() {
    return {
      formDataChanged: false
    };
  },
  methods: {
    saveData() {
      // 保存数据的方法
      console.log('保存数据');
    }
  }
};
</script>

在上述代码中,beforeRouteLeave 守卫用于检查是否有未保存的数据。如果有未保存的数据,会提示用户是否保存;然后允许路由跳转。

5.3.2 源码分析

resolveAsyncComponents 函数中,虽然没有直接处理 beforeRouteLeave 守卫,但在路由切换时,会在合适的时机执行该守卫。具体的执行逻辑和其他组件内守卫类似,会在 confirmTransition 方法中处理。

六、路由守卫的执行顺序

6.1 执行顺序概述

路由守卫的执行顺序如下:

  1. 全局前置守卫(beforeEach

  2. 路由独享守卫(beforeEnter

  3. 组件内守卫:

    • beforeRouteEnter
    • beforeRouteUpdate(如果路由更新)
  4. 全局解析守卫(beforeResolve

  5. 路由切换完成

  6. 全局后置守卫(afterEach

  7. 组件内守卫:

    • beforeRouteLeave(在离开组件时执行)

6.2 示例代码分析

以下是一个包含所有类型路由守卫的示例代码,通过打印日志来观察它们的执行顺序:

javascript

javascript 复制代码
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const Home = {
  template: '<div>Home Page</div>',
  beforeRouteEnter(to, from, next) {
    console.log('Home 组件 beforeRouteEnter');
    next();
  },
  beforeRouteLeave(to, from, next) {
    console.log('Home 组件 beforeRouteLeave');
    next();
  }
};

const About = {
  template: '<div>About Page</div>',
  beforeRouteEnter(to, from, next) {
    console.log('About 组件 beforeRouteEnter');
    next();
  },
  beforeRouteLeave(to, from, next) {
    console.log('About 组件 beforeRouteLeave');
    next();
  }
};

const routes = [
  {
    path: '/',
    component: Home,
    beforeEnter: (to, from, next) {
      console.log('Home 路由 beforeEnter');
      next();
    }
  },
  {
    path: '/about',
    component: About,
    beforeEnter: (to, from, next) {
      console.log('About 路由 beforeEnter');
      next();
    }
  }
];

const router = new VueRouter({
  routes
});

router.beforeEach((to, from, next) => {
  console.log('全局前置守卫 beforeEach');
  next();
});

router.beforeResolve((to, from, next) => {
  console.log('全局解析守卫 beforeResolve');
  next();
});

router.afterEach((to, from) => {
  console.log('全局后置守卫 afterEach');
});

new Vue({
  router
}).$mount('#app');

当从 / 导航到 /about 时,控制台输出的日志顺序如下:

plaintext

javascript 复制代码
全局前置守卫 beforeEach
Home 路由 beforeEnter
Home 组件 beforeRouteLeave
About 路由 beforeEnter
About 组件 beforeRouteEnter
全局解析守卫 beforeResolve
全局后置守卫 afterEach

通过这个示例,我们可以清楚地看到路由守卫的执行顺序。

七、路由守卫的高级应用

7.1 权限验证

路由守卫最常见的应用场景之一是权限验证。通过在全局前置守卫或路由独享守卫中检查用户的登录状态或权限,可以确保只有授权用户才能访问某些路由。以下是一个更复杂的权限验证示例:

javascript

javascript 复制代码
// 模拟用户登录状态
const isAuthenticated = () => {
  return localStorage.getItem('token') !== null;
};

// 模拟用户角色
const getUserRole = () => {
  return localStorage.getItem('role');
};

const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      requiresAdmin: true
    }
  },
  {
    path: '/user',
    component: User,
    meta: {
      requiresAuth: true
    }
  },
  {
    path: '/login',
    component: Login
  }
];

const router = new VueRouter({
  routes
});

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // 如果需要登录权限且用户未登录,重定向到登录页面
    next({ path: '/login' });
  } else if (to.meta.requiresAdmin && getUserRole()!== 'admin') {
    // 如果需要管理员权限且用户不是管理员,跳转到无权限页面
    next({ name: 'NoAccess' });
  } else {
    // 允许路由跳转
    next();
  }
});

在上述代码中,我们通过检查路由的 meta 属性来判断目标路由是否需要登录权限或管理员权限。如果用户未登录或没有相应的权限,则重定向到相应的页面。

7.2 数据预加载

在路由切换前,可以使用路由守卫进行数据预加载,避免在页面渲染后再发起请求,提高用户体验。以下是一个示例:

vue

javascript 复制代码
<template>
  <div>
    <h1>{{ user.name }}</h1>
  </div>
</template>

<script>
export default {
  name: 'User',
  data() {
    return {
      user: {}
    };
  },
  beforeRouteEnter(to, from, next) {
    // 模拟异步数据请求
    const fetchUser = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({ name: 'John Doe' });
        }, 1000);
      });
    };

    fetchUser().then(user => {
      next(vm => {
        vm.user = user;
      });
    });
  }
};
</script>

在上述代码中,beforeRouteEnter 守卫会在路由进入组件前发起异步数据请求,获取用户信息。在数据请求完成后,通过 next 函数的回调将数据赋值给组件实例的 user 属性。

7.3 页面过渡效果控制

可以使用路由守卫来控制页面的过渡效果。例如,在路由切换前添加过渡动画,在路由切换完成后移除动画。以下是一个示例:

vue

javascript 复制代码
<template>
  <div id="app">
    <transition name="fade">
      <router-view></router-view>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    startTransition() {
      // 开始过渡动画的逻辑
      console.log('开始过渡动画');
    },
    endTransition() {
      // 结束过渡动画的逻辑
      console.log('结束过渡动画');
    }
  },
  beforeRouteLeave(to, from, next) {
    this.startTransition();
    next();
  },
  beforeRouteEnter(to, from, next) {
    next(vm => {
      vm.$nextTick(() => {
        vm.endTransition();
      });
    });
  }
};
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

在上述代码中,beforeRouteLeave 守卫会在路由离开组件前开始过渡动画,beforeRouteEnter 守卫会在路由进入组件后结束过渡动画。

八、路由守卫的错误处理

8.1 守卫中抛出错误

在路由守卫中,如果发生错误,可以抛出一个 Error 实例,导航会被终止,并且该错误会被传递给 router.onError() 注册的回调函数。以下是一个示例:

javascript

javascript 复制代码
router.beforeEach((to, from, next) => {
    try {
        // 模拟一个可能出错的操作
        const result = someFunctionThatMightThrowError();
        if (result) {
            next();
        } else {
            // 抛出错误
            throw new Error('路由守卫中发生错误');
        }
    } catch (error) {
        // 将错误传递给 next 函数
        next(error);
    }
});

// 注册错误处理回调函数
router.onError((error) => {
    console.error('路由导航出错:', error.message);
    // 可以在这里进行错误处理,例如显示错误页面
});

在上述代码中,beforeEach 守卫中尝试调用一个可能会抛出错误的函数 someFunctionThatMightThrowError。如果发生错误,会捕获该错误并通过 next(error) 将其传递给路由系统。router.onError 注册的回调函数会接收这个错误并进行处理,这里只是简单地将错误信息打印到控制台,实际应用中可以显示一个错误页面。

源码分析

confirmTransition 方法中,当守卫函数抛出错误时,会调用 abort 函数进行处理:

javascript

javascript 复制代码
class History {
    confirmTransition(route, onComplete, onAbort) {
        const current = this.current;
        const abort = err => {
            if (isError(err)) {
                if (this.router.onError) {
                    // 调用错误处理回调函数
                    this.router.onError(err); 
                }
                if (onAbort) {
                    onAbort(err);
                }
            } else if (onAbort) {
                onAbort();
            }
        };

        const resolveQueue = [
            ...this.router.beforeHooks,
            ...this.getBeforeEnterHooks(route),
            ...resolveAsyncComponents(route),
            ...this.router.resolveHooks
        ];

        const iterator = (hook, next) => {
            try {
                hook(route, current, (to) => {
                    if (to === false) {
                        abort();
                    } else if (isError(to)) {
                        abort(to);
                    } else if (typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string'))) {
                        this.transitionTo(to, onComplete, abort);
                    } else {
                        next();
                    }
                });
            } catch (e) {
                // 捕获守卫函数抛出的错误并调用 abort 函数
                abort(e); 
            }
        };

        runQueue(resolveQueue, iterator, () => {
            this.updateRoute(route);
            onComplete && onComplete(route);
            this.router.afterHooks.forEach(hook => {
                hook(route, current);
            });
        });
    }
}

iterator 函数中,使用 try...catch 块捕获守卫函数抛出的错误,然后调用 abort 函数。abort 函数会检查错误是否为 Error 实例,如果是,则调用 router.onError 注册的回调函数进行处理。

8.2 异步守卫中的错误处理

在异步守卫中,例如在 beforeRouteEnter 中进行异步数据请求时,如果请求失败,也需要进行错误处理。以下是一个示例:

vue

javascript 复制代码
<template>
    <div>
        <!-- 组件内容 -->
    </div>
</template>

<script>
export default {
    name: 'User',
    beforeRouteEnter(to, from, next) {
        // 模拟异步数据请求
        const fetchUser = () => {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    // 模拟请求失败
                    reject(new Error('数据请求失败')); 
                }, 1000);
            });
        };

        fetchUser()
          .then(user => {
                next(vm => {
                    vm.user = user;
                });
            })
          .catch(error => {
                // 处理请求失败的情况
                next({ name: 'ErrorPage', params: { errorMessage: error.message } });
            });
    },
    data() {
        return {
            user: {}
        };
    }
};
</script>

在上述代码中,beforeRouteEnter 守卫中发起了一个异步数据请求。如果请求失败,会捕获错误并通过 next 函数将用户重定向到错误页面,同时传递错误信息。

源码分析

异步守卫的错误处理逻辑和同步守卫类似,在异步操作的 catch 块中捕获错误并通过 next 函数传递给路由系统。在 confirmTransition 方法中,会对 next 函数传递的错误进行处理,最终调用 abort 函数,将错误传递给 router.onError 注册的回调函数。

8.3 错误处理的最佳实践

  • 明确错误类型:在抛出错误时,尽量使用有意义的错误类型和错误信息,方便调试和定位问题。例如,可以自定义错误类型:

javascript

javascript 复制代码
class AuthError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AuthError';
    }
}

router.beforeEach((to, from, next) => {
    if (!isAuthenticated()) {
        next(new AuthError('用户未登录'));
    } else {
        next();
    }
});
  • 统一错误处理 :使用 router.onError 注册一个全局的错误处理回调函数,将所有路由导航中的错误统一处理。这样可以避免在每个守卫中重复编写错误处理代码。
  • 显示友好的错误信息:在错误处理回调函数中,显示友好的错误信息给用户,而不是直接将错误信息暴露给用户。例如,可以显示一个错误页面,提示用户发生了什么错误以及如何解决。
  • 日志记录 :在错误处理回调函数中,记录错误信息到日志中,方便后续分析和排查问题。可以使用第三方日志库,如 SentryLogRocket

九、路由守卫与路由元信息的结合使用

9.1 路由元信息的概念

路由元信息是在路由配置中通过 meta 属性添加的额外信息。这些信息可以用于权限验证、页面标题设置、路由过渡效果控制等。以下是一个简单的路由配置示例,包含路由元信息:

javascript

javascript 复制代码
const routes = [
    {
        path: '/',
        component: Home,
        meta: {
            title: '首页',
            requiresAuth: false
        }
    },
    {
        path: '/dashboard',
        component: Dashboard,
        meta: {
            title: '仪表盘',
            requiresAuth: true
        }
    }
];

在上述代码中,每个路由都有一个 meta 属性,包含了页面标题和是否需要登录权限的信息。

9.2 结合路由守卫进行权限验证

可以结合路由元信息和路由守卫进行权限验证。在全局前置守卫中,检查目标路由的 meta 属性,判断是否需要登录权限。以下是一个示例:

javascript

javascript 复制代码
router.beforeEach((to, from, next) => {
    const isAuthenticated = () => {
        return localStorage.getItem('token') !== null;
    };

    if (to.meta.requiresAuth && !isAuthenticated()) {
        next({ path: '/login' });
    } else {
        next();
    }
});

在上述代码中,全局前置守卫会检查目标路由的 meta.requiresAuth 属性。如果需要登录权限且用户未登录,则重定向到登录页面;否则,允许路由跳转。

9.3 结合路由守卫设置页面标题

可以结合路由元信息和路由守卫来动态设置页面标题。在全局后置守卫中,根据目标路由的 meta 属性设置页面标题。以下是一个示例:

javascript

javascript 复制代码
router.afterEach((to, from) => {
    if (to.meta.title) {
        document.title = to.meta.title;
    }
});

在上述代码中,全局后置守卫会检查目标路由的 meta.title 属性。如果存在,则将页面标题设置为该属性的值。

9.4 源码分析

在路由匹配过程中,会将路由元信息存储在路由记录中。在 addRouteRecord 函数中,会将 meta 属性添加到路由记录中:

javascript

javascript 复制代码
function addRouteRecord(pathList, routeMap, route, parent) {
    const path = parent ? normalizePath(route.path, parent) : route.path;
    const record = {
        path,
        name: route.name,
        component: route.component,
        parent,
        // 存储路由元信息
        meta: route.meta || {} 
    };

    if (route.children) {
        route.children.forEach(child => {
            addRouteRecord(pathList, routeMap, child, record);
        });
    }

    if (!routeMap[path]) {
        pathList.push(path);
        routeMap[path] = record;
    }
}

在路由守卫中,可以通过 to.metafrom.meta 访问目标路由和当前路由的元信息。例如,在全局前置守卫中:

javascript

javascript 复制代码
router.beforeEach((to, from, next) => {
    // 访问目标路由的元信息
    if (to.meta.requiresAuth) { 
        // 进行权限验证
    }
    next();
});

十、路由守卫与导航守卫的生命周期钩子的关系

10.1 导航守卫的生命周期钩子概述

导航守卫的生命周期钩子包括全局前置守卫、全局解析守卫、全局后置守卫、路由独享守卫和组件内守卫。这些钩子在路由切换的不同阶段执行,形成了一个完整的导航流程。

10.2 与组件生命周期钩子的对比

组件生命周期钩子是 Vue 组件自身的生命周期方法,如 beforeCreatecreatedbeforeMountmounted 等。而导航守卫是 Vue Router 提供的用于控制路由切换的钩子。它们的主要区别如下:

  • 作用范围:组件生命周期钩子只对组件自身生效,而导航守卫可以在全局、路由级别和组件级别生效。
  • 执行时机:组件生命周期钩子在组件实例创建、挂载、更新和销毁等阶段执行,而导航守卫在路由切换的不同阶段执行。
  • 功能用途:组件生命周期钩子主要用于组件的初始化、数据获取、DOM 操作等,而导航守卫主要用于控制路由的跳转、权限验证、数据预加载等。

10.3 结合使用示例

可以结合导航守卫和组件生命周期钩子来实现更复杂的功能。例如,在 beforeRouteEnter 守卫中进行数据预加载,在组件的 created 钩子中使用预加载的数据:

vue

javascript 复制代码
<template>
    <div>
        <h1>{{ user.name }}</h1>
    </div>
</template>

<script>
export default {
    name: 'User',
    data() {
        return {
            user: {}
        };
    },
    beforeRouteEnter(to, from, next) {
        const fetchUser = () => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    resolve({ name: 'John Doe' });
                }, 1000);
            });
        };

        fetchUser().then(user => {
            next(vm => {
                // 将预加载的数据传递给组件实例
                vm.user = user; 
            });
        });
    },
    created() {
        // 可以直接使用预加载的数据
        console.log('用户信息:', this.user); 
    }
};
</script>

在上述代码中,beforeRouteEnter 守卫中进行了异步数据请求,将获取到的数据传递给组件实例。在组件的 created 钩子中,可以直接使用预加载的数据。

10.4 源码分析

导航守卫和组件生命周期钩子的执行是相互独立的,但在路由切换过程中会有一定的顺序。在 confirmTransition 方法中,会依次执行导航守卫,然后在路由切换完成后,组件会触发自身的生命周期钩子。例如,在组件挂载时,会依次触发 beforeCreatecreatedbeforeMountmounted 等钩子。

十一、路由守卫的性能优化

11.1 减少不必要的守卫

在使用路由守卫时,要避免在不必要的地方使用守卫。例如,如果某个路由不需要进行权限验证,就不要在该路由的配置中添加权限验证的守卫。过多的守卫会增加路由切换的时间开销,影响性能。

11.2 异步守卫的优化

在异步守卫中,要尽量减少异步操作的时间。可以使用缓存、预加载等技术来优化异步数据请求。例如,使用 localStoragesessionStorage 缓存数据,避免重复请求:

vue

javascript 复制代码
<template>
    <div>
        <!-- 组件内容 -->
    </div>
</template>

<script>
export default {
    name: 'User',
    beforeRouteEnter(to, from, next) {
        const cachedUser = localStorage.getItem('user');
        if (cachedUser) {
            const user = JSON.parse(cachedUser);
            next(vm => {
                vm.user = user;
            });
        } else {
            const fetchUser = () => {
                return new Promise((resolve) => {
                    setTimeout(() => {
                        const user = { name: 'John Doe' };
                        localStorage.setItem('user', JSON.stringify(user));
                        resolve(user);
                    }, 1000);
                });
            };

            fetchUser().then(user => {
                next(vm => {
                    vm.user = user;
                });
            });
        }
    },
    data() {
        return {
            user: {}
        };
    }
};
</script>

在上述代码中,beforeRouteEnter 守卫会先检查本地存储中是否有缓存的用户数据。如果有,则直接使用缓存数据;否则,发起异步数据请求,并将获取到的数据缓存到本地存储中。

11.3 守卫的顺序优化

合理安排守卫的顺序可以提高路由切换的性能。例如,将一些简单的检查放在前面,将复杂的异步操作放在后面。这样可以在早期快速判断是否需要继续执行后续的守卫,减少不必要的计算。

11.4 源码分析

confirmTransition 方法中,守卫队列的执行是依次进行的。如果某个守卫的执行时间过长,会影响整个路由切换的性能。因此,在编写守卫时,要尽量减少守卫的执行时间,避免在守卫中进行复杂的计算或长时间的异步操作。

十二、路由守卫在不同路由模式下的表现

12.1 hash 模式下的路由守卫

在 hash 模式下,路由的切换是通过 URL 的哈希值变化来实现的。当哈希值改变时,会触发 hashchange 事件,从而执行路由守卫。以下是一个简单的示例:

javascript

javascript 复制代码
const router = new VueRouter({
    mode: 'hash',
    routes
});

router.beforeEach((to, from, next) => {
    console.log('hash 模式下的全局前置守卫');
    next();
});

在 hash 模式下,路由守卫的执行逻辑和其他模式下基本相同。当用户点击链接或手动修改 URL 的哈希值时,会触发路由切换,依次执行导航守卫。

12.2 history 模式下的路由守卫

在 history 模式下,路由的切换是通过 HTML5 的 History API 来实现的。当用户点击链接或调用 pushStatereplaceState 方法时,会触发路由切换,执行路由守卫。以下是一个示例:

javascript

javascript 复制代码
const router = new VueRouter({
    mode: 'history',
    routes
});

router.beforeEach((to, from, next) => {
    console.log('history 模式下的全局前置守卫');
    next();
});

在 history 模式下,需要注意的是,服务器端需要进行相应的配置,以支持单页面应用的路由。否则,当用户直接访问某个路由时,服务器可能会返回 404 错误。

12.3 abstract 模式下的路由守卫

abstract 模式通常用于非浏览器环境,如 Node.js 服务器端渲染。在这种模式下,路由的切换是通过内部的历史记录栈来管理的。路由守卫的执行逻辑和其他模式下基本相同。以下是一个示例:

javascript

javascript 复制代码
const router = new VueRouter({
    mode: 'abstract',
    routes
});

router.beforeEach((to, from, next) => {
    console.log('abstract 模式下的全局前置守卫');
    next();
});

在 abstract 模式下,由于不依赖于浏览器的历史记录,因此可以在非浏览器环境中使用 Vue Router 进行路由管理。

12.4 源码分析

在不同的路由模式下,路由守卫的执行逻辑在 confirmTransition 方法中是统一处理的。不同模式下的主要区别在于路由切换的触发方式和历史记录的管理方式。例如,在 hash 模式下,会监听 hashchange 事件;在 history 模式下,会使用 pushStatereplaceState 方法来管理历史记录。

十三、路由守卫的测试

13.1 单元测试

可以使用单元测试框架(如 Jest)来对路由守卫进行单元测试。以下是一个对全局前置守卫进行单元测试的示例:

javascript

javascript 复制代码
import Vue from 'vue';
import VueRouter from 'vue-router';
import { beforeEachGuard } from './router';

Vue.use(VueRouter);

describe('全局前置守卫测试', () => {
    let router;
    let to;
    let from;
    let next;

    beforeEach(() => {
        const routes = [
            { path: '/', component: { template: '<div>Home</div>' } },
            { path: '/about', component: { template: '<div>About</div>' } }
        ];
        router = new VueRouter({ routes });
        to = { path: '/about' };
        from = { path: '/' };
        next = jest.fn();
    });

    it('应该允许路由跳转', () => {
        beforeEachGuard(to, from, next);
        expect(next).toHaveBeenCalled();
    });

    it('应该重定向到登录页面', () => {
        // 模拟需要登录权限的情况
        to.meta = { requiresAuth: true };
        const isAuthenticated = jest.fn(() => false);
        beforeEachGuard(to, from, next);
        expect(next).toHaveBeenCalledWith({ path: '/login' });
    });
});

在上述代码中,使用 Jest 框架对全局前置守卫进行了单元测试。通过模拟 tofromnext 参数,测试了守卫在不同情况下的行为。

13.2 集成测试

集成测试可以测试路由守卫在整个应用中的表现。可以使用测试工具(如 Vue Test Utils)来进行集成测试。以下是一个对路由守卫进行集成测试的示例:

javascript

javascript 复制代码
import { mount } from '@vue/test-utils';
import App from './App.vue';
import router from './router';

describe('路由守卫集成测试', () => {
    it('应该重定向到登录页面', async () => {
        const wrapper = mount(App, {
            global: {
                plugins: [router]
            }
        });
        await router.push('/dashboard');
        expect(router.currentRoute.value.path).toBe('/login');
    });
});

在上述代码中,使用 Vue Test Utils 对路由守卫进行了集成测试。通过 mount 函数挂载应用,并使用 router.push 方法进行路由跳转,然后检查当前路由是否符合预期。

13.3 测试的最佳实践

  • 模拟环境 :在测试中,要模拟路由守卫的执行环境,包括 tofromnext 参数。可以使用 Jest 的 jest.fn() 来模拟 next 函数。
  • 覆盖不同情况:测试要覆盖路由守卫的不同情况,例如允许路由跳转、重定向、取消导航等。
  • 结合快照测试:可以结合快照测试来验证路由守卫的输出结果是否符合预期。
相关推荐
江城开朗的豌豆4 分钟前
Git分支管理:从'独狼开发'到'团队协作'的进化之路
前端·javascript·面试
GIS之家5 分钟前
vue+cesium示例:3D热力图(附源码下载)
前端·vue.js·3d·cesium·webgis·3d热力图
幽蓝计划6 分钟前
鸿蒙Next仓颉语言开发实战教程:下拉刷新和上拉加载更多
前端
红衣信7 分钟前
电影项目开发中的编程要点与用户体验优化
前端·javascript·github
LeeAt12 分钟前
npm:详细解释前端项目开发前奏!!
前端·node.js·html
山有木兮木有枝_14 分钟前
JavaScript对象深度解析:从创建到类型判断 (上)
前端
crary,记忆21 分钟前
MFE(微前端) Module Federation:Webpack.config.js文件中每个属性的含义解释
前端·学习·webpack
清风~徐~来24 分钟前
【Qt】控件 QWidget
前端·数据库·qt
前端小白从0开始24 分钟前
关于前端常用的部分公共方法(二)
前端·vue.js·正则表达式·typescript·html5·公共方法
真的很上进31 分钟前
2025最全TS手写题之partial/Omit/Pick/Exclude/Readonly/Required
java·前端·vue.js·python·算法·react·html5