MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Router 导航守卫的使用场景与功能扩展

2024-09-302.0k 阅读

Vue Router 导航守卫基础概念

在 Vue Router 中,导航守卫是一种非常强大的机制,它可以在路由导航发生的各个阶段进行拦截和控制。简单来说,导航守卫就像是一个关卡,在路由跳转的不同时机可以执行特定的代码逻辑,决定是否允许导航继续进行。

Vue Router 提供了三种类型的导航守卫:全局守卫、路由独享守卫和组件内守卫。

全局守卫

全局守卫可以应用于整个应用程序的所有路由导航。它包括 beforeEachbeforeResolveafterEach

  • beforeEach:这是最常用的全局前置守卫,在每次路由导航之前都会被调用。它接收三个参数:to(即将要进入的目标路由对象)、from(当前导航正要离开的路由对象)和 next(一个函数,用于控制导航的流程)。如果不调用 next,导航将被中断。
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import About from '@/components/About';

Vue.use(Router);

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
});

router.beforeEach((to, from, next) => {
  console.log('Before Each Guard: from', from.path, 'to', to.path);
  next();
});

export default router;

在上述代码中,每次路由跳转前都会在控制台打印出当前跳转的起始路径和目标路径,并通过 next() 继续导航。

  • beforeResolve:全局解析守卫,和 beforeEach 类似,但它在所有组件内守卫和异步路由组件被解析之后,beforeEach 之后,afterEach 之前被调用。同样接收 tofromnext 参数。
router.beforeResolve((to, from, next) => {
  console.log('Before Resolve Guard: from', from.path, 'to', to.path);
  next();
});
  • afterEach:全局后置钩子,在路由导航完成之后被调用。它接收两个参数:tofrom,但不接收 next 函数,因为此时导航已经完成,不能再对导航进行控制。通常用于记录页面访问日志等操作。
router.afterEach((to, from) => {
  console.log('After Each Guard: from', from.path, 'to', to.path);
});

路由独享守卫

路由独享守卫是直接定义在路由配置中的守卫。目前只有 beforeEnter 这一种类型。它只在进入特定路由时被调用,接收 tofromnext 三个参数,用法和全局守卫中的 beforeEach 类似。

const router = new Router({
  routes: [
    {
      path: '/secret',
      name: 'Secret',
      component: SecretComponent,
      beforeEnter: (to, from, next) => {
        const isAuthenticated = true; // 这里假设一个认证状态
        if (isAuthenticated) {
          next();
        } else {
          next('/login');
        }
      }
    }
  ]
});

在上述代码中,当尝试访问 /secret 路由时,会检查 isAuthenticated 变量。如果用户已认证,则允许进入该路由;否则,将导航到 /login 路由。

组件内守卫

组件内守卫是定义在 Vue 组件内部的导航守卫。包括 beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

  • beforeRouteEnter:在进入组件的路由被确认前调用。此时组件实例还未被创建,所以不能使用 this。可以通过 next 回调函数传递一个回调给 vm(组件实例),该回调会在组件被创建后调用。
export default {
  name: 'MyComponent',
  data() {
    return {
      message: 'Initial message'
    };
  },
  beforeRouteEnter(to, from, next) {
    console.log('Before Route Enter in MyComponent');
    next(vm => {
      console.log('Inside next callback, component instance is available:', vm.message);
    });
  }
};
  • beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用。比如从 /user/1 导航到 /user/2,组件实例会被复用,此时会调用 beforeRouteUpdate。这里可以访问 this
export default {
  name: 'UserComponent',
  data() {
    return {
      user: null
    };
  },
  beforeRouteUpdate(to, from, next) {
    this.fetchUser(to.params.userId);
    next();
  },
  methods: {
    fetchUser(userId) {
      // 模拟异步获取用户数据
      setTimeout(() => {
        this.user = { id: userId, name: 'User' + userId };
      }, 1000);
    }
  }
};
  • beforeRouteLeave:在离开当前组件的路由时调用。通常用于用户未保存表单数据时提示用户确认离开。这里也可以访问 this
export default {
  name: 'EditFormComponent',
  data() {
    return {
      formData: {
        name: '',
        email: ''
      },
      isFormDirty: false
    };
  },
  methods: {
    onInput() {
      this.isFormDirty = true;
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.isFormDirty) {
      const confirmLeave = window.confirm('You have unsaved changes. Are you sure you want to leave?');
      if (confirmLeave) {
        next();
      } else {
        next(false);
      }
    } else {
      next();
    }
  }
};

Vue Router 导航守卫的使用场景

身份验证与授权

身份验证和授权是导航守卫最常见的使用场景之一。在许多应用程序中,某些页面只允许已登录的用户访问,而其他页面可能根据用户的角色有不同的访问权限。

  • 身份验证:通过全局前置守卫 beforeEach 可以实现对所有路由的身份验证检查。例如,假设应用程序使用 JWT(JSON Web Token)进行身份验证,可以在 beforeEach 中检查本地存储中是否存在有效的 JWT。
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token');
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth &&!token) {
    next('/login');
  } else {
    next();
  }
});

在上述代码中,通过 to.matched.some(record => record.meta.requiresAuth) 检查目标路由是否需要身份验证。如果需要且用户未登录(即没有 token),则导航到 /login 页面。

  • 授权:授权可以基于用户角色来控制对特定路由的访问。例如,只有管理员角色的用户才能访问管理页面。可以在路由配置中添加 meta 字段来标记需要特定角色访问的路由。
const router = new Router({
  routes: [
    {
      path: '/admin',
      name: 'Admin',
      component: AdminComponent,
      meta: {
        requiresRole: 'admin'
      }
    }
  ]
});

router.beforeEach((to, from, next) => {
  const userRole = localStorage.getItem('userRole');
  const requiresRole = to.matched.some(record => record.meta.requiresRole);
  if (requiresRole && (!userRole || userRole!== to.meta.requiresRole)) {
    next('/forbidden');
  } else {
    next();
  }
});

这里在 beforeEach 中检查用户角色是否符合目标路由的要求,如果不符合则导航到 /forbidden 页面。

页面访问权限控制

除了基于身份验证和授权的访问控制,导航守卫还可以用于更细粒度的页面访问权限控制。例如,某些页面可能只允许在特定条件下访问,比如用户必须完成特定的引导流程。

router.beforeEach((to, from, next) => {
  const isOnboardingCompleted = localStorage.getItem('isOnboardingCompleted') === 'true';
  const requiresOnboarding = to.matched.some(record => record.meta.requiresOnboarding);
  if (requiresOnboarding &&!isOnboardingCompleted) {
    next('/onboarding');
  } else {
    next();
  }
});

在这个例子中,如果目标路由标记了需要完成引导流程(通过 meta.requiresOnboarding),并且用户尚未完成引导流程(通过 localStorage 检查),则导航到 /onboarding 页面。

数据预加载

在进入某些页面之前,可能需要预先加载一些数据,以确保页面能够快速呈现给用户。导航守卫可以方便地实现这一功能。例如,在进入用户详情页面之前,预先加载用户的详细信息。

export default {
  name: 'UserDetailComponent',
  data() {
    return {
      user: null
    };
  },
  beforeRouteEnter(to, from, next) {
    const userId = to.params.userId;
    // 模拟异步数据加载
    setTimeout(() => {
      const user = { id: userId, name: 'User' + userId };
      next(vm => {
        vm.user = user;
      });
    }, 1000);
  }
};

beforeRouteEnter 中,根据路由参数 userId 模拟异步加载用户数据,并通过 next 回调将数据传递给组件实例。

防止用户误操作

在一些情况下,用户可能会意外地离开某个页面,而此时可能有未保存的工作。通过 beforeRouteLeave 组件内守卫可以提示用户确认离开,防止数据丢失。

<template>
  <div>
    <textarea v-model="content" @input="markAsDirty"></textarea>
  </div>
</template>

<script>
export default {
  name: 'EditPage',
  data() {
    return {
      content: '',
      isDirty: false
    };
  },
  methods: {
    markAsDirty() {
      this.isDirty = true;
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.isDirty) {
      const confirmLeave = window.confirm('You have unsaved changes. Are you sure you want to leave?');
      if (confirmLeave) {
        next();
      } else {
        next(false);
      }
    } else {
      next();
    }
  }
};
</script>

在这个文本编辑页面的例子中,当用户输入内容时,isDirty 标志被设置为 true。当用户尝试离开页面时,beforeRouteLeave 会弹出确认框,根据用户的选择决定是否继续导航。

Vue Router 导航守卫的功能扩展

自定义导航守卫中间件

虽然 Vue Router 提供了基本的导航守卫类型,但在大型应用中,可能需要更复杂的逻辑处理。可以通过创建自定义导航守卫中间件来实现。例如,创建一个用于处理通用权限验证逻辑的中间件。

const permissionMiddleware = (to, from, next) => {
  const userRole = localStorage.getItem('userRole');
  const requiresRole = to.matched.some(record => record.meta.requiresRole);
  if (requiresRole && (!userRole || userRole!== to.meta.requiresRole)) {
    next('/forbidden');
  } else {
    next();
  }
};

router.beforeEach(permissionMiddleware);

通过将通用的权限验证逻辑封装在 permissionMiddleware 函数中,可以在多个路由守卫中复用,使代码更加模块化和可维护。

结合 Vuex 进行状态管理

Vuex 是 Vue.js 应用程序的状态管理模式。导航守卫可以与 Vuex 结合,根据 Vuex 中的状态来控制路由导航。例如,在 Vuex 中存储用户的登录状态和角色信息,在导航守卫中读取这些状态进行身份验证和授权。

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    isLoggedIn: false,
    userRole: null
  },
  mutations: {
    setLoggedIn(state, value) {
      state.isLoggedIn = value;
    },
    setUserRole(state, role) {
      state.userRole = role;
    }
  }
});

// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import Admin from '@/components/Admin';
import store from './store';

Vue.use(Router);

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/admin',
      name: 'Admin',
      component: Admin,
      meta: {
        requiresRole: 'admin'
      }
    }
  ]
});

router.beforeEach((to, from, next) => {
  const isLoggedIn = store.state.isLoggedIn;
  const userRole = store.state.userRole;
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  const requiresRole = to.matched.some(record => record.meta.requiresRole);
  if (requiresAuth &&!isLoggedIn) {
    next('/login');
  } else if (requiresRole && (!userRole || userRole!== to.meta.requiresRole)) {
    next('/forbidden');
  } else {
    next();
  }
});

export default router;

在上述代码中,导航守卫从 Vuex 的状态中读取用户的登录状态和角色信息,根据这些信息进行身份验证和授权,实现更灵活和可扩展的路由控制。

动态路由加载与导航守卫结合

在一些应用中,可能需要根据用户的权限动态加载路由。导航守卫可以在动态路由加载的过程中发挥重要作用,确保只有符合权限的路由被加载和访问。

// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import store from './store';

Vue.use(Router);

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    }
  ]
});

const loadAdminRoutes = () => {
  const adminRoutes = [
    {
      path: '/admin/dashboard',
      name: 'AdminDashboard',
      component: () => import('@/components/AdminDashboard'),
      meta: {
        requiresRole: 'admin'
      }
    }
  ];
  router.addRoutes(adminRoutes);
};

router.beforeEach((to, from, next) => {
  const userRole = store.state.userRole;
  if (userRole === 'admin' &&!router.hasRoute('AdminDashboard')) {
    loadAdminRoutes();
  }
  next();
});

export default router;

在这个例子中,当检测到用户角色为 adminAdminDashboard 路由尚未加载时,通过 router.addRoutes 动态加载管理相关的路由。导航守卫确保了动态路由加载与权限控制的紧密结合。

导航守卫与动画效果结合

导航守卫还可以与 Vue 的动画效果结合,实现更流畅的页面过渡体验。例如,在路由切换时添加淡入淡出的动画效果。

<template>
  <div id="app">
    <transition name="fade">
      <router-view></router-view>
    </transition>
  </div>
</template>

<script>
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import About from '@/components/About';

Vue.use(Router);

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
});

router.beforeEach((to, from, next) => {
  document.documentElement.classList.add('router-transition');
  next();
});

router.afterEach(() => {
  setTimeout(() => {
    document.documentElement.classList.remove('router-transition');
  }, 300);
});

export default router;
</script>

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

.router-transition {
  overflow: hidden;
}
</style>

在上述代码中,通过在 beforeEach 中添加 router-transition 类,在 afterEach 中延迟移除该类,结合 CSS 的过渡效果,实现了路由切换时的淡入淡出动画。这种结合方式可以提升用户体验,使页面导航更加自然和流畅。

导航守卫使用注意事项

避免无限循环

在使用导航守卫时,特别是在 next 函数的调用上,要特别小心避免无限循环。例如,如果在 beforeEach 守卫中,根据某些条件始终调用 next 并指向当前路由,就会导致无限循环。

// 错误示例
router.beforeEach((to, from, next) => {
  const condition = true;
  if (condition) {
    next(to.path); // 这会导致无限循环
  } else {
    next();
  }
});

正确的做法是确保 next 调用的目标路由不会导致回到当前路由,除非有明确的终止条件。

性能考虑

虽然导航守卫非常强大,但过多或复杂的导航守卫逻辑可能会影响应用程序的性能。特别是在全局守卫中,如果执行了大量的计算或异步操作,可能会导致路由导航变得缓慢。因此,要尽量保持导航守卫逻辑的简洁,将复杂的业务逻辑封装到独立的函数或模块中,并尽量减少异步操作在导航守卫中的执行。

守卫之间的顺序与协同

不同类型的导航守卫(全局、路由独享、组件内)在路由导航过程中有特定的执行顺序。了解这些顺序对于正确编写和调试导航守卫逻辑非常重要。全局守卫 beforeEach 会在所有其他守卫之前执行,然后是路由独享的 beforeEnter,接着是组件内的 beforeRouteEnter。在导航确认之后,会执行全局的 beforeResolve,最后执行 afterEach。在编写多个导航守卫时,要确保它们之间的协同工作,避免出现逻辑冲突。

例如,在全局 beforeEach 中进行了基本的身份验证,而在组件内的 beforeRouteEnter 中进行更细粒度的权限检查,要确保这两个守卫的逻辑相互配合,不会出现重复或矛盾的情况。

导航守卫在实际项目中的应用案例

电商应用中的用户权限控制

在一个电商应用中,有普通用户、商家和管理员三种角色。不同角色有不同的访问权限。例如,普通用户可以浏览商品、下单等;商家可以管理自己的店铺、商品等;管理员可以进行全局的系统设置、用户管理等。

// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home';
import ProductList from '@/components/ProductList';
import MerchantDashboard from '@/components/MerchantDashboard';
import AdminDashboard from '@/components/AdminDashboard';
import store from './store';

Vue.use(Router);

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/products',
      name: 'ProductList',
      component: ProductList
    },
    {
      path: '/merchant/dashboard',
      name: 'MerchantDashboard',
      component: MerchantDashboard,
      meta: {
        requiresRole:'merchant'
      }
    },
    {
      path: '/admin/dashboard',
      name: 'AdminDashboard',
      component: AdminDashboard,
      meta: {
        requiresRole: 'admin'
      }
    }
  ]
});

router.beforeEach((to, from, next) => {
  const userRole = store.state.userRole;
  const requiresRole = to.matched.some(record => record.meta.requiresRole);
  if (requiresRole && (!userRole || userRole!== to.meta.requiresRole)) {
    next('/forbidden');
  } else {
    next();
  }
});

export default router;

通过导航守卫,根据用户在 Vuex 中存储的角色信息,控制用户对不同页面的访问,确保系统的安全性和功能的合理性。

单页应用的多语言切换与路由导航

在一个国际化的单页应用中,需要根据用户选择的语言动态加载相应语言的页面内容。可以结合导航守卫和 Vuex 来实现这一功能。

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    locale: 'en'
  },
  mutations: {
    setLocale(state, locale) {
      state.locale = locale;
    }
  }
});

// router.js
import Vue from 'vue';
import Router from 'vue-router';
import HomeEn from '@/components/HomeEn';
import HomeZh from '@/components/HomeZh';
import store from './store';

Vue.use(Router);

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => {
        if (store.state.locale === 'en') {
          return import('@/components/HomeEn');
        } else {
          return import('@/components/HomeZh');
        }
      }
    }
  ]
});

router.beforeEach((to, from, next) => {
  const currentLocale = store.state.locale;
  // 这里可以添加更多逻辑,比如检查语言是否支持等
  next();
});

export default router;

在这个例子中,导航守卫虽然没有直接进行复杂的逻辑处理,但结合 Vuex 中的语言状态,在路由配置中动态加载不同语言版本的组件,实现了多语言切换与路由导航的协同工作。

通过以上对 Vue Router 导航守卫的深入探讨,包括其基础概念、使用场景、功能扩展、注意事项以及实际应用案例,相信开发者能够更好地利用导航守卫来构建灵活、安全且用户体验良好的 Vue.js 应用程序。无论是小型项目还是大型企业级应用,导航守卫都为路由导航的控制提供了强大而灵活的工具。