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

Vue项目中的路由守卫使用场景

2021-04-014.4k 阅读

1. 路由守卫基础概念

在 Vue 项目中,路由守卫是一种非常强大的功能,它允许我们在路由跳转的不同阶段执行特定的逻辑。路由守卫主要分为全局守卫、路由独享守卫和组件内守卫。

1.1 全局守卫

全局守卫作用于整个 Vue 应用的路由,主要有 beforeEachbeforeResolveafterEach

  • beforeEach:这是最常用的全局前置守卫。它在每次路由跳转之前都会被调用,接收三个参数:to(即将要进入的目标路由对象)、from(当前导航正要离开的路由对象)和 next(一个函数,调用该函数来决定是否继续导航)。如果不调用 next,导航将被中断。示例代码如下:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

const router = new Router({
  routes: [
    {
      path: '/home',
      name: 'Home',
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/Login.vue')
    }
  ]
})

router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token')
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (isLoggedIn) {
      next()
    } else {
      next({ name: 'Login' })
    }
  } else {
    next()
  }
})

export default router

在上述代码中,我们定义了一个全局前置守卫 beforeEach。当用户访问需要认证的路由(通过 meta.requiresAuth 判断)时,会检查 localStorage 中是否存在 token。如果存在,则允许进入目标路由;否则,将用户重定向到登录页面。

  • beforeResolve:全局解析守卫,它和 beforeEach 很相似,也是在每次路由跳转之前调用,但它在所有组件内守卫和异步路由组件被解析之后调用。这意味着在 beforeResolve 中,你可以确保所有异步组件都已经加载完成。它同样接收 tofromnext 三个参数。例如,当你有一些依赖于异步组件加载完成后的逻辑时,可以使用 beforeResolve
router.beforeResolve((to, from, next) => {
  // 这里可以处理异步组件加载完成后的逻辑
  next()
})
  • afterEach:全局后置钩子,它在每次路由跳转完成后被调用,接收两个参数 tofrom。与前置守卫不同的是,它不接收 next 函数,因为此时导航已经完成,无法中断。常用于记录页面访问日志等场景。
router.afterEach((to, from) => {
  console.log(`从 ${from.path} 跳转到了 ${to.path}`)
})

1.2 路由独享守卫

路由独享守卫是直接定义在路由配置中的守卫,只对特定的路由生效。目前只有 beforeEnter 这一种类型。它接收 tofromnext 三个参数,用法和全局前置守卫类似。例如,我们要对某个特定的路由进行权限检查:

const router = new Router({
  routes: [
    {
      path: '/admin',
      name: 'Admin',
      component: () => import('@/views/Admin.vue'),
      beforeEnter: (to, from, next) => {
        const isAdmin = localStorage.getItem('role') === 'admin'
        if (isAdmin) {
          next()
        } else {
          next({ name: 'Home' })
        }
      }
    }
  ]
})

在上述代码中,只有当用户角色为 admin 时,才能访问 /admin 路由,否则将被重定向到首页。

1.3 组件内守卫

组件内守卫是定义在 Vue 组件内部的守卫,包括 beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

  • beforeRouteEnter:在渲染该组件的路由被确认前调用,此时组件实例还未被创建,所以不能使用 this。可以通过 next 回调函数来访问组件实例。例如,我们要在进入某个组件前根据路由参数加载数据:
<template>
  <div>
    <h1>{{ data }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      data: null
    }
  },
  beforeRouteEnter(to, from, next) {
    // 模拟异步获取数据
    setTimeout(() => {
      const data = `从路由参数获取的数据: ${to.params.id}`
      next(vm => {
        vm.data = data
      })
    }, 1000)
  }
}
</script>

在上述代码中,beforeRouteEnter 守卫在组件渲染前被调用,通过 setTimeout 模拟异步操作获取数据,然后通过 next 回调函数将数据传递给组件实例。

  • beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用,例如从 /user/1 跳转到 /user/2,组件会被复用。此时可以访问组件实例 this。常用于根据新的路由参数更新组件数据。
<template>
  <div>
    <h1>{{ user }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  beforeRouteUpdate(to, from, next) {
    // 根据新的路由参数获取用户数据
    this.user = `用户 ${to.params.id}`
    next()
  }
}
</script>

在上述代码中,当路由参数改变时,beforeRouteUpdate 守卫会被调用,根据新的路由参数更新 user 数据。

  • beforeRouteLeave:在导航离开该组件的对应路由时调用,可以访问组件实例 this。常用于用户离开页面时的提示,例如用户是否保存未提交的表单数据等。
<template>
  <div>
    <form>
      <input type="text" v-model="inputValue">
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      inputValue: ''
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.inputValue) {
      const confirmLeave = window.confirm('你有未保存的数据,确定离开吗?')
      if (confirmLeave) {
        next()
      } else {
        next(false)
      }
    } else {
      next()
    }
  }
}
</script>

在上述代码中,当用户试图离开当前路由且输入框中有内容时,会弹出确认框询问用户是否确定离开。

2. 常见使用场景

2.1 身份验证

身份验证是路由守卫最常见的使用场景之一。在许多应用中,某些页面只允许已登录用户访问,而其他页面则对所有用户开放。通过路由守卫,我们可以轻松实现这一功能。

以一个简单的博客应用为例,用户可以查看文章列表(公开页面),但撰写文章、编辑文章等功能需要用户登录。我们可以在路由配置中设置 meta.requiresAuth 来标记需要认证的路由,然后在全局前置守卫 beforeEach 中进行检查。

const router = new Router({
  routes: [
    {
      path: '/articles',
      name: 'Articles',
      component: () => import('@/views/Articles.vue')
    },
    {
      path: '/write-article',
      name: 'WriteArticle',
      component: () => import('@/views/WriteArticle.vue'),
      meta: { requiresAuth: true }
    },
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/Login.vue')
    }
  ]
})

router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token')
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (isLoggedIn) {
      next()
    } else {
      next({ name: 'Login' })
    }
  } else {
    next()
  }
})

在上述代码中,当用户尝试访问 /write - article 路由时,全局前置守卫会检查 localStorage 中是否存在 token。如果不存在,用户将被重定向到登录页面。

2.2 权限控制

除了身份验证,权限控制也是路由守卫的重要应用场景。不同角色的用户可能具有不同的访问权限,例如管理员可以访问所有页面,普通用户只能访问部分页面。

假设我们有一个后台管理系统,有管理员和普通用户两种角色。管理员可以访问用户管理、系统设置等所有页面,而普通用户只能访问个人信息页面。我们可以在路由配置中设置 meta.role 来标记不同路由所需的角色,然后在全局前置守卫中进行检查。

const router = new Router({
  routes: [
    {
      path: '/user - management',
      name: 'UserManagement',
      component: () => import('@/views/UserManagement.vue'),
      meta: { role: 'admin' }
    },
    {
      path: '/system - settings',
      name: 'SystemSettings',
      component: () => import('@/views/SystemSettings.vue'),
      meta: { role: 'admin' }
    },
    {
      path: '/personal - info',
      name: 'PersonalInfo',
      component: () => import('@/views/PersonalInfo.vue'),
      meta: { role: ['admin', 'user'] }
    },
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/Login.vue')
    }
  ]
})

router.beforeEach((to, from, next) => {
  const userRole = localStorage.getItem('role')
  if (to.matched.some(record => record.meta.role)) {
    const requiredRole = record.meta.role
    if (Array.isArray(requiredRole)) {
      if (requiredRole.includes(userRole)) {
        next()
      } else {
        next({ name: 'Login' })
      }
    } else {
      if (userRole === requiredRole) {
        next()
      } else {
        next({ name: 'Login' })
      }
    }
  } else {
    next()
  }
})

在上述代码中,当用户尝试访问 /user - management 路由时,全局前置守卫会检查用户角色是否为 admin。如果不是,用户将被重定向到登录页面。

2.3 数据预加载

在某些情况下,我们希望在进入某个页面之前提前加载相关数据,以提高用户体验。路由守卫可以帮助我们实现这一功能。

例如,在一个商品详情页面,我们需要根据商品 ID 从服务器获取商品的详细信息。我们可以在 beforeRouteEnter 组件内守卫中进行数据预加载。

<template>
  <div>
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  data() {
    return {
      product: null
    }
  },
  beforeRouteEnter(to, from, next) {
    const productId = to.params.productId
    axios.get(`/api/products/${productId}`)
    .then(response => {
        next(vm => {
          vm.product = response.data
        })
      })
    .catch(error => {
        console.error('获取商品数据失败', error)
        next(false)
      })
  }
}
</script>

在上述代码中,beforeRouteEnter 守卫在组件渲染前被调用,通过 axios 发送请求获取商品数据。获取成功后,通过 next 回调函数将数据传递给组件实例。

2.4 防止未保存数据丢失

当用户在填写表单等操作时,如果不小心导航离开页面,可能会导致未保存的数据丢失。通过路由守卫,我们可以在用户离开页面时进行提示,询问用户是否保存数据。

以一个简单的表单组件为例,我们可以在 beforeRouteLeave 组件内守卫中实现这一功能。

<template>
  <div>
    <form>
      <input type="text" v-model="formData.name">
      <input type="email" v-model="formData.email">
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        name: '',
        email: ''
      }
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.formData.name || this.formData.email) {
      const confirmLeave = window.confirm('你有未保存的数据,确定离开吗?')
      if (confirmLeave) {
        next()
      } else {
        next(false)
      }
    } else {
      next()
    }
  }
}
</script>

在上述代码中,当用户试图离开当前路由且表单中有填写内容时,会弹出确认框询问用户是否确定离开。

2.5 多语言切换

在国际化应用中,我们可能需要根据用户选择的语言切换页面内容。路由守卫可以帮助我们在路由跳转时根据用户设置的语言偏好加载相应的语言资源。

假设我们使用 vue - i18n 进行多语言支持,我们可以在全局前置守卫中根据用户选择的语言设置 vue - i18n 的语言。

import Vue from 'vue'
import Router from 'vue - router'
import VueI18n from 'vue - i18n'

Vue.use(Router)
Vue.use(VueI18n)

const i18n = new VueI18n({
  locale: localStorage.getItem('locale') || 'en',
  messages: {
    en: {
      welcome: 'Welcome'
    },
    zh: {
      welcome: '欢迎'
    }
  }
})

const router = new Router({
  routes: [
    {
      path: '/home',
      name: 'Home',
      component: () => import('@/views/Home.vue')
    }
  ]
})

router.beforeEach((to, from, next) => {
  const locale = localStorage.getItem('locale')
  if (locale) {
    i18n.locale = locale
  }
  next()
})

export default router

在上述代码中,每次路由跳转时,全局前置守卫会检查 localStorage 中保存的语言设置,并将 vue - i18n 的语言设置为相应的值。

3. 注意事项

3.1 next 函数的使用

在路由守卫中,next 函数的正确使用至关重要。如果忘记调用 next,导航将被中断,用户可能会停留在当前页面。此外,next 函数可以接收参数,例如 next(false) 可以取消导航,next({ path: '/new - route' }) 可以重定向到新的路由。在使用时要确保根据业务逻辑正确调用 next 函数。

3.2 守卫中的异步操作

当在路由守卫中进行异步操作时,例如异步获取数据或调用 API,要注意处理异步操作的结果。如果异步操作失败,应该通过 next(false) 或重定向到合适的路由来处理错误。同时,要避免在守卫中进行过多复杂的异步操作,以免影响路由跳转的性能。

3.3 组件内守卫与全局守卫的优先级

组件内守卫 beforeRouteEnter 的优先级高于全局前置守卫 beforeEach。当路由跳转时,首先会调用 beforeRouteEnter,然后再调用 beforeEach。了解这一优先级顺序有助于我们正确编写守卫逻辑,避免出现逻辑冲突。

3.4 守卫中的 this 指向

在组件内守卫 beforeRouteEnter 中,由于组件实例还未创建,不能直接使用 this。而在 beforeRouteUpdatebeforeRouteLeave 中,可以正常使用 this 来访问组件实例的属性和方法。在全局守卫和路由独享守卫中,也不能直接使用 this,需要通过其他方式来访问全局状态或组件实例。

4. 总结与实践建议

路由守卫是 Vue 路由系统中一个非常强大且灵活的功能,通过合理使用路由守卫,我们可以实现身份验证、权限控制、数据预加载等多种功能,从而提升应用的安全性和用户体验。

在实践中,建议根据应用的业务需求合理选择使用全局守卫、路由独享守卫还是组件内守卫。对于通用的逻辑,如身份验证和权限控制,使用全局守卫可以提高代码的复用性;对于特定路由的特殊逻辑,使用路由独享守卫更为合适;而对于与组件紧密相关的逻辑,如组件数据预加载和防止未保存数据丢失,则应使用组件内守卫。

同时,要注意路由守卫中 next 函数的正确使用,以及异步操作的处理。在编写守卫逻辑时,要保持代码的简洁和可读性,避免过度复杂的逻辑导致维护困难。通过不断实践和总结经验,我们可以更好地利用路由守卫来构建健壮、高效的 Vue 应用。

在实际项目中,还可以结合 Vuex 等状态管理工具,将用户认证状态、权限信息等存储在 Vuex 中,在路由守卫中通过访问 Vuex 状态来进行更灵活的逻辑判断。例如,在全局前置守卫中,可以根据 Vuex 中存储的用户角色信息来进行权限控制,这样可以使代码结构更加清晰,便于维护和扩展。

另外,随着项目规模的扩大,路由守卫的数量和复杂度可能会增加。此时,可以考虑将相关的守卫逻辑封装成函数或模块,以提高代码的可维护性和复用性。例如,将身份验证的逻辑封装成一个单独的函数,在多个路由守卫中调用,这样当身份验证逻辑发生变化时,只需要在一个地方进行修改即可。

总之,路由守卫是 Vue 开发中不可或缺的一部分,熟练掌握和运用路由守卫可以帮助我们开发出更加优质的前端应用。通过不断学习和实践,我们能够更好地发挥路由守卫的强大功能,满足各种复杂的业务需求。