vue2动态路由实现与遇见的问题和处理方法

1,077 阅读4分钟

使用的是很老的admin 后台模板。 我这边使用的动态路由权限验证方法是和网上大多不一样的。不是后端返回路由结构,然后前端直接根据结构渲染就行了,而是把结构放在前端本地, 然后前端根据权限去做筛选。

我这里是后端直接把一个人全部的权限都塞在一个数组中,一起返给我。大到菜单的权限,小到一个按钮的权限,都是放在一起的。

大概是这样的。

image.png

由于在权限对象中是有一级菜单和二级菜单标记的,这个时候我们只需要先拿到请求的数组先筛选, 把一级菜单和二级菜单都筛选出来。

代码如下:

// 我是直接把筛选封装成一个方法
// 筛选请求结果(父路由)
const menuList = (arr) => {
  return arr.filter(item => {
    return item.type === 0 // 这里type 0 是一级菜单标识
  })
}
// 筛选请求结果(子路由)
const childrenMenuList = (arr) => {
  return arr.filter(item => { // 这里type 1 是二级菜单标识
    return item.type === 1
  })
}

筛选出父子权限路由之后。就需要把我们存在前端的完整的asyncRoutes,拿来比较匹配就行

方法如下:

// 我是父路由和子路由分开匹配的,也就是两个方法,父路由一个匹配方法,子路由一个匹配方法
const RoutesFilter = (resRoutes, asyncRoutes) => { // 使用筛选出来的动态父路由权限匹配出父路由数组
  const newRoutes = []
  for (let i = 0; i < asyncRoutes.length; i++) {
    const asyncRoute = asyncRoutes[i]
    for (let j = 0; j < resRoutes.length; j++) {
      const resRoute = resRoutes[j]
      if (asyncRoute.name === resRoute.name) {
        newRoutes.push(asyncRoute)
      }
    }
  }
  return newRoutes
}

const childrenRouteFilter = (resChildrenRoutes, asyncChildrenRoutes) => { // 使用筛选出来的动态子路由权限匹配出子路由数组
  const newChildrenRoutes = []
  for (let i = 0; i < asyncChildrenRoutes.length; i++) {
    const asyncChildrenRoute = asyncChildrenRoutes[i]
    for (let j = 0; j < resChildrenRoutes.length; j++) {
      const resChildrenRoute = resChildrenRoutes[j]
      if (asyncChildrenRoute.name === resChildrenRoute.name) {
        newChildrenRoutes.push(asyncChildrenRoute)
      }
    }
  }
  return newChildrenRoutes
}

这里可能有人会疑惑。 为什么不直接递归或者一个方法多嵌套几层循环,非要写两个方法还父子路由分开匹配。

我说下我的思路,第一,目前我做的这个项目就只有一个模块有子菜单,所以我是直接把子菜单单独摘出来放在一个childrenAsyncRoutes数组中管理。也就是说,以后如果有增加模块和其他模块增加子菜单,我都可以丢这里面,统一管理。说直白点就是把所有的子菜单都放在一个数组中。第二,是为了思路清晰,这样做是不会被深层的循环嵌套给绕晕的,操作上也比较简单。

现在方法封装好了,接下来就可以在vuex的actions里,做请求权限数据的操作了。

const actions = {
  async generateRoutes({ commit }) {
    try {
      const res = await store.dispatch('user/userInfoAction') // 请求用户信息权限
      const menuArr = menuList(res.data.menu) // 筛选父路由的动态权限
      const childrenMenuArr = childrenMenuList(res.data.menu) // 筛选子路由的动态权限

      const newRoutes = RoutesFilter(menuArr, asyncRoutes) // 父路由权限匹配方法
      const newChildrenRoutes = childrenRouteFilter(childrenMenuArr, asyncChildrenRoutes) // 子路由权限匹配方法

      const notChildrenRoutes = constantRoutes.concat(newRoutes) // 匹配出来的父路由于静态路由合并

      // 目前只有系统管理模块有子路由所以,只需要找系统管理就可以了。如果之后有更多的模块拥有子路由那么需要在拥有子路由的父路由中增加一个识别状态,用来判断。
      notChildrenRoutes.forEach(item => {
        if (item.name === '系统管理') {
          // 把上方匹配出来的子路由与静态子路由合并,再排序
          item.children = bubbleSort(item.children.concat(newChildrenRoutes))
        }
      })

      // 由于动态路由在刷新页面时会找不到路由而跳转到404页面。所有需要把404通配符* 从静态路由组中拿出来,使用动态加载到路由最后面
      const routes = notChildrenRoutes.concat(route404)

      // 对整个拼接出来的路由重新排序
      const routesSortArr = bubbleSort(routes)

      // 把排序好的路由数组赋值给router的原路由参数,把原路由参数替换成新的
      router.options.routes = routesSortArr

      // 由于使用router.addRoutes()方法注册新的router.options.routes会报路由名重复的警告
      // 是因为路由信息还没初始化,信息里还存在原路由信息导致的路由名重复的警告
      // 这里需要重新实例化一个Router,然后使用实例中的matcher把原router中的matcher初始化一下,就解决了报路由名重复的警告
      router.matcher = new Router().matcher

      // 在初始化之后, 再使用router.addRoutes()方法注册一下替换的路由。就成功激活路由了
      router.addRoutes(router.options.routes)

      // filterRoutes(menuArr, constantRoutes) // 根据权限设置父路由状态
      // filterChildrenRoutes(childrenMenuArr, constantRoutes) // 根据权限设置子路由状态

      commit('SET_ROUTES', { routes, menu: res.data.menu })
      return Promise.resolve(constantRoutes)
    } catch (err) {
      throw new Error(err)
    }
  }
}

在vue2中的router 在渲染动态路由上是有bug的,在渲染出动态路由之后,只要一刷新就会由于vuex中的数据还没有请求回来引起的找不到路由而跳往404或者空白页面。

所以这里需要注意。 上方代码中,在筛选完匹配完形成一个新的路由数组之后,使用router在注册新路由数组时的解决方案,这也是我百度了很久,找到的解决方案。

这里还需要提一下的是, 由于筛选路由数组的时候都是拼接的,所以路由菜单的顺序都是错乱的。这里我就使用冒泡排序重新排了一下序,在路由中可以把序号写在meta字段中,直接跟着序号排序就行。

我也把排序方法贴上来吧

function bubbleSort(arr) { // 路由从小到大排序
  const len = arr.length
  for (let i = 0; i < len; i++) {
    for (let j = 1; j < len - i; j++) {
      if (arr[j - 1].meta.sort > arr[j].meta.sort) {
        [arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
      }
    }
  }
  return arr
}

我再把存在前端的路由结构也贴出来仅供参考:

image.png

这里可能需要说下。404为啥要单独放。

是因为不能在自己写的路由里边添加404 要在addRoutes中添加404页面不然就会跳转404 所以我把代码改成把通配符 * 跳转404 页面添加路由最后,这样就解决了。

最后再说一下。 在导航守卫中的发请求的操作和处理重复进导航守卫发请求的操作。

router.beforeEach(async(to, from, next) => {
  NProgress.start()
  document.title = getPageTitle(to.meta.title)
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      next()
      NProgress.done()
    } else {
    // 这里利用vuex中的数据来处理重复请求的操作
      const hasRoute = store.state.permission.newRoutes && store.state.permission.newRoutes.length > 0
      if (hasRoute) {
        next()
        NProgress.done()
      } else {
        try {
          await store.dispatch('permission/generateRoutes') // 权限菜单

          next({ ...to, replace: true }) // 这里一定要加,replace,不然跳转会出问题。
          NProgress.done()
        } catch (error) {
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})