SPA 别陷入路由死循环

在 vue 路由中,我们会用导航守卫控制页面的能否被进入查看,其中最后一步是调用 next() 函数,让中断的导航继续进行

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。所以确保要调用 next 方法,否则钩子就不会被 resolved

路由循环

关键点:

next () 可以传入参数,实现跳转到其他页面或者取消导航的功能。这时候要注意,不能堵住通道,要确保每一个条件最后都能调用没有传递参数的 next() 方法
否则,页面会陷入无限刷新的死循环。因为传入参数的 next() 方法会导致路由再次执行前置导航守卫,从而陷入循环。

例子1:验证 token ,失败则返回登录页

router.beforeEach((to,from,next) =>{
  if (sessionStorage.getItem("token")) {
     if(to.path === "/login"){
       next({path:"/dashboard"})
     }
     else{
       alert("1")
       next()
     }     
  }else{
    next({path: "/login"})   // 会再次执行前置导航守卫,因为路径变化
  }
})

上面的代码表面看没有问题:

如果 sessionStorage 有 token,并且即将要进入的目标路径是登陆页,就跳转到 /dashboard 页,如果是其它的页面,就直接进入

如果 sessionStorage 没有 token 就跳转到登陆页

但是代码执行会引起死循环,原因是没有出口,执行 next({path: "/login"}) 会再次执行全局前置导航守卫。进入 /login 页面前,就再次触发守卫,一直重复进入

代码改成下面的就正常了:

router.beforeEach((to, from, next) => {
  let token = window.sessionStorage.getItem('token');
  if (to.path != '/login' && !token) {
    next({
      path: '/login'
    })
  } else {
    if (to.path == '/login' && token) {
      next('/dashboard')
    } else {     
      next()
    }
  }
})

例子2:动态添加路由

router.beforeEach((to, from, next) => {
  function getRouteAndMenu () {
    // 本地没有保存可访问路由,就需要计算
    store.dispatch('d2admin/user/GenerateRoutes') // 获取可访问路由,在 vuex 中保存
    router.addRoutes(store.state.d2admin.user.accessedRouters) // 和原有的固定路由合并到一起
    const routeArray = routes.concat(store.state.d2admin.user.accessedRouters)
    // 处理路由 得到每一级的路由设置
    store.commit('d2admin/page/init', routeArray)
    // 设置顶栏菜单
    store.dispatch('d2admin/menu/GenerateHeaderMenu', {
      role: store.state.d2admin.user.info.role,
      menuHeader
    }) 
    // 设置侧边栏菜单
    store.dispatch('d2admin/menu/GenerateMenu', {
      role: store.state.d2admin.user.info.role,
      menuAside
    }) 
    // 获取侧边栏菜单,在 vuex 中保存
    store.dispatch('d2admin/menu/setMenuAside', {
      menuAside
    })
    next({ ...to, replace: true })  // hack 以确保路由增加后,再进行跳转
  }

  if (from.name === null && to.name === '404') {
    // 避免刷新出现 404 页面
    getRouteAndMenu()
  }
  
  // 验证当前路由所有的匹配中是否需要有登录验证的
  if (to.matched.some(r => r.meta.auth)) {
    // 这里暂时将cookie里是否存有token作为验证是否登录的条件
    // 请根据自身业务需要修改
    const token = util.cookies.get('token')
    if (token && token !== 'undefined') {
      const accessedRouters = store.state.d2admin.user.accessedRouters
      if (accessedRouters.length <= 0) {
        getRouteAndMenu()
      } else {
        next()
      }
    } else {
      // 没有登录的时候跳转到登录界面
      // 携带上登陆成功之后需要跳转的页面完整路径
      next({
        name: 'login',
        query: {
          redirect: to.fullPath
        }
      })
    }
  } else {
    // 不需要身份校验 直接通过
    next()
  }
})

上面的代码,主要是实现计算动态路由并添加到原有路由里、计算可展示的菜单栏,然后再跳转到相应页面的功能。

其中,next({ ...to, replace: true })方法是一个 hack 方法,能确保路由添加了再进行跳转

问题在于,这里只将动态计算的路由保存到了内存,所以在页面刷新时,需要重新计算并添加,所以添加了下面的代码:

if (from.name === null && to.name === '404') {
    // 避免刷新出现 404 页面
    getRouteAndMenu()
}

而这会导致页面一直在重复执行 getRouteAndMenu() 操作,浏览器会重复导航进入当前刷新的页面,

原因:getRouteAndMenu 除了 hack 方法没有提供 next() 通道,每次 hack 都会再次执行导航守卫

解决方法:

不用 hack ,改成使用 async await ,按顺序执行,最后 next()

router.beforeEach((to, from, next) => {
  async function getRouteAndMenu () {
    // 本地没有保存可访问路由,就需要计算
    await store.dispatch('d2admin/user/GenerateRoutes') // 获取可访问路由,在 vuex 中保存
    router.addRoutes(store.state.d2admin.user.accessedRouters) // 和原有的固定路由合并到一起
    const routeArray = routes.concat(store.state.d2admin.user.accessedRouters)
    // 处理路由 得到每一级的路由设置
    store.commit('d2admin/page/init', routeArray)
    // 设置顶栏菜单
    await store.dispatch('d2admin/menu/GenerateHeaderMenu', {
      role: store.state.d2admin.user.info.role,
      menuHeader
    }) 
    // 设置侧边栏菜单
    await store.dispatch('d2admin/menu/GenerateMenu', {
      role: store.state.d2admin.user.info.role,
      menuAside
    }) 
    // 获取侧边栏菜单,在 vuex 中保存
    await store.dispatch('d2admin/menu/setMenuAside', {
      menuAside
    })
    next()
  }

  if (from.name === null && to.name === '404') {
    // 避免刷新出现 404 页面
    getRouteAndMenu()
  }
  
  // 验证当前路由所有的匹配中是否需要有登录验证的
  if (to.matched.some(r => r.meta.auth)) {
    // 这里暂时将cookie里是否存有token作为验证是否登录的条件
    // 请根据自身业务需要修改
    const token = util.cookies.get('token')
    if (token && token !== 'undefined') {
      const accessedRouters = store.state.d2admin.user.accessedRouters
      if (accessedRouters.length <= 0) {
        getRouteAndMenu()
      } else {
        next()
      }
    } else {
      // 没有登录的时候跳转到登录界面
      // 携带上登陆成功之后需要跳转的页面完整路径
      next({
        name: 'login',
        query: {
          redirect: to.fullPath
        }
      })
    }
  } else {
    // 不需要身份校验 直接通过
    next()
  }
})

另附路由导航守卫的调用顺序

Vue Router 导航解析流程.jpg

阅读 386

推荐阅读