前端权限管理之 addRoutes 动态加载路由踩坑

栏目: 编程语言 · 发布时间: 5年前

内容简介:这几天在开发后台管理系统的路由权限,在开始做之前,我查阅了不少资料,发现前后端分离的权限管理基本就以下两种方式:第一种,完全由后端控制路由,但这也意味着如果前端需要修改或者增减路由都需要经过后端大大的同意,也是我司目前采用的方式;第二种,相对于第一种,前端相对会自由一些,但是如果角色权限发生了改变就需要前后端一起修改,而且如果某些(技术型)用户在前端修改了自己的角色权限就可以通过路由看到一些本不被允许看到的页面,虽然拿不到数据,但是有些页面还是不希望被不相关的人看到(虽然我个人jio得并没有什么关系,但是无

这几天在开发后台管理系统的路由权限,在开始做之前,我查阅了不少资料,发现前后端分离的权限管理基本就以下两种方式:

addRoutes

两种方法的不同

第一种,完全由后端控制路由,但这也意味着如果前端需要修改或者增减路由都需要经过后端大大的同意,也是我司目前采用的方式;

第二种,相对于第一种,前端相对会自由一些,但是如果角色权限发生了改变就需要前后端一起修改,而且如果某些(技术型)用户在前端修改了自己的角色权限就可以通过路由看到一些本不被允许看到的页面,虽然拿不到数据,但是有些页面还是不希望被不相关的人看到(虽然我个人jio得并没有什么关系,但是无奈leader还是偏向不想被看到不该看到的页面)。

接下来我主要讲一下第一种方式得做法以及踩的一些坑。

addRoutes 需要的数据格式

官方文档

router.addRoutes

函数签名:

router.addRoutes(routes: Array<RouteConfig>)
复制代码

动态添加更多的路由规则。参数必须是一个符合 routes 选项要求的数组。

前端初始化路由

个人认为 addRoutes 可以理解为往现有的路由后面添加新的路由,所以在 addRoutes 之前我们需要初始化一些不需要权限的路由页面,比如登录页、首页、404页面等,这个过程很简单,就是往路由文件里面加入静态路由就行了,这里就不赘述了。

接下来就是设计后端路由表,确定前后端交互的数据格式。

设计后端路由表

字段名 说明
*id id
*pid 父级id
*path 路由路径
name 路由名称
*component 路由组件路径
redirect 重定向路径
hidden 是否隐藏
meta 标识

* 的为必有字段

接收后端生成的路由并解析

通过上面设计的路由表可以发现路由之间时是通过 pid 来确定上下级的,所以在接收到后端传来的路由数据时我们需要在前端解析成符合 addRoutes 入参的格式。

在接收到后端生成的路由后通过以下函数进行解析成相应的格式:

parse_routes.js

import Router from '@/router'

/**
 * @desc: 解析原始路由信息(路由之间通过pid确定上下级)并动态添加路由及跳转页面
 * @param {Array} menus - (从后端获取的)菜单路由信息
 * @param {String} to - 解析成功后需要跳转的路由路径
 * @example
 * // 引入parse_routes
 * const menus = [ // 由后端传入
 *  { "id": 1, "pid": 0, "path": "/receipt", "name": "", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": "" },
 *  { "id": 2, "pid": 1, "path": "index", "name": "Receipt", "component": "receipt/index", "redirect": "","hidden": "false", "meta": "{\"title\": \"收款管理\", \"icon\": \"receipt\"}" },
 *  { "id": 3, "pid": 0, "path": "/payment", "name": "", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": "" },
 *  { "id": 4, "pid": 3, "path": "index", "name": "Payment", "component": "payment/index", "redirect": "", "hidden": "false", "meta": "{\"title\": \"付款管理\", \"icon\": \"payment\"}" },
 *  { "id": 5, "pid": 0, "path": "/crm", "name":"", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": "" },
 *  { "id": 6, "pid": 5, "path": "index","name": "Crm", "component": "crm/index", "redirect": "","hidden": "false", "meta": "{\"title\": \"客户管理\", \"icon\": \"people\"}" },
 *  { "id": 7, "pid": 0, "path": "/upload_product", "name":"", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": ""},
 *  { "id": 8, "pid": 7, "path": "index","name": "productUpload", "component": "productUpload/index", "redirect": "","hidden": "false", "meta": "{\"title\": \"测评商品上传\", \"icon\": \"upload\"}" }
 * ]
 * ParseRoutes(menus, '/payment/index')
 */
export default (menus, to = '/') => {
  // 初始路由
  const defRoutes = [
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/login/index'),
      hidden: true
    },
    {
      path: '/',
      component: () => import('@/views/layout/Layout'),
      redirect: '/dashboard',
      name: 'Dashboard',
      children: [
        {
          path: 'dashboard',
          meta: { title: '首页', icon: 'home' },
          component: () => import('@/views/dashboard/index')
        }
      ]
    },
    {
      path: '/404',
      name: '404',
      component: () => import('@/views/404'),
      hidden: true
    },
    {
      path: '*',
      redirect: '/404',
      hidden: true
    }
  ]

  // 初始化路由信息对象
  const menusMap = {}
  menus.map(v => {
    const { path, name, component, redirect, hidden, meta } = v
    // 重新构建路由对象
    const item = {
      path,
      name,
      component: () => import(`@/views/${component}`),
      redirect,
      hidden: JSON.parse(hidden)
    }
    meta.length !== 0 && (item.meta = JSON.parse(meta))
    // 判断是否为根节点
    if (v.pid === 0) {
      menusMap[v.id] = item
    } else {
      !menusMap[v.pid].children && (menusMap[v.pid].children = [])
      menusMap[v.pid].children.push(item)
    }
  })

  // 将生成数组树结构的菜单
  const routes = Object.values(menusMap)
  // 默认路由拼接生成的路由(注意顺序)
  const integralRoutes = defRoutes.concat(routes)

  Router.options.routes = integralRoutes
  Router.addRoutes(routes)
  Router.push({ path: to })
}

复制代码

渲染侧边栏菜单

在成功解析数据之后就需要渲染侧边栏了,我这里参考的是大佬(PanJiaChen)的 element-ui-admin ,具体可以参考大佬的代码,这里也不再赘述了。

如果坚持看到了这里,那么恭喜你,基本就可以通过 addRoutes 动态加载路由了。

接下来就开始讲我在使用 addRoutes 的过程中遇到的一些坑。(读者心里os: mmp,终于进入正题了~)

重点难点1:跳转页面后404

在我们成功动态添加路由后,改变地址栏或者刷新页面,你会发现页面跳到了404。

根据我们上面的路由配置:

[
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/login/index'),
      hidden: true
    },
    {
      path: '/',
      component: () => import('@/views/layout/Layout'),
      redirect: '/dashboard',
      name: 'Dashboard',
      children: [
        {
          path: 'dashboard',
          meta: { title: '首页', icon: 'home' },
          component: () => import('@/views/dashboard/index')
        }
      ]
    },
    {
      path: '/404',
      name: '404',
      component: () => import('@/views/404'),
      hidden: true
    },
    {
      path: '*',
      redirect: '/404',
      hidden: true
    }
  ]
复制代码

你会发现我们在这里面初始化了404路由,所以在路由没有找到强匹配的地址时,就会跳转到404页面。

解决的方法很多,我们这里只讲一种。

解决方案

就是不在初始化路由的时候初始化404路由,而是在 解析接收到的路由数据时拼接路由 即可解决问题。

parse_routes.js

...
// 将生成数组树结构的菜单并拼接404路由
  const routes = Object.values(menusMap).concat(notFoundRoutes)
复制代码

重点难点2:刷新页面路由失效

解决了404的问题后,再次刷新页面会发现页面变空白了,这是因为刷新页面router实例会重新初始化到初始状态。

解决方案

我们在获取到后端数据的时候将之存入 vuex 和 浏览器缓存 (我用的是 sessionStorage) 中。注意,这里是将获取到的数据直接存入,因为 sessionStorage 只能存字符串,而我们在转换格式的过程中是需要解析某些字段,例如 component , hidden 等。

actions.js

...
const menus = data.data.menus
// 将获取到的数据存入 sessionStorage 和 vuex 中
sessionStorage.setItem('_c_unparseRoutes', JSON.stringify(menus))
commit('GET_ROUTES', menus) // 解析函数
ParseRoutes(menus)
复制代码

然后在 App.vue 中的钩子函数 created() 或者 mounted() 中检测 vuex 中的数据是否为空且 sessionStorage 中是否有存入关的数据,并监听页面刷新。

App.vue

...
created() {
  const unparseRoutes = JSON.parse(sessionStorage.getItem('_c_unparseRoutes'))
  if (this.localRoutes.length === 0 && unparseRoutes) {
    const toPath = sessionStorage.getItem('_c_lastPath')
    ParseRoutes(unparseRoutes, toPath) // 解析函数
  }
  // 监听页面刷新
  window.addEventListener('beforeunload', () => {
    sessionStorage.setItem('_c_lastPath', this.$router.currentRoute.path)
  })
}
复制代码

解析函数(完整版)

import Router from '@/router'

/**
 * @desc: 解析原始路由信息(路由之间通过pid确定上下级)并动态添加路由及跳转页面
 * @param {Array} menus - (从后端获取的)菜单路由信息
 * @param {String} to - 解析成功后需要跳转的路由路径
 * @example
 * // 引入parse_routes
 * const menus = [ // 由后端传入
 *  { "id": 1, "pid": 0, "path": "/receipt", "name": "", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": "" },
 *  { "id": 2, "pid": 1, "path": "index", "name": "Receipt", "component": "receipt/index", "redirect": "","hidden": "false", "meta": "{\"title\": \"收款管理\", \"icon\": \"receipt\"}" },
 *  { "id": 3, "pid": 0, "path": "/payment", "name": "", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": "" },
 *  { "id": 4, "pid": 3, "path": "index", "name": "Payment", "component": "payment/index", "redirect": "", "hidden": "false", "meta": "{\"title\": \"付款管理\", \"icon\": \"payment\"}" },
 *  { "id": 5, "pid": 0, "path": "/crm", "name":"", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": "" },
 *  { "id": 6, "pid": 5, "path": "index","name": "Crm", "component": "crm/index", "redirect": "","hidden": "false", "meta": "{\"title\": \"客户管理\", \"icon\": \"people\"}" },
 *  { "id": 7, "pid": 0, "path": "/upload_product", "name":"", "component": "layout/Layout", "redirect": "", "hidden": "false", "meta": ""},
 *  { "id": 8, "pid": 7, "path": "index","name": "productUpload", "component": "productUpload/index", "redirect": "","hidden": "false", "meta": "{\"title\": \"测评商品上传\", \"icon\": \"upload\"}" }
 * ]
 * ParseRoutes(menus, '/payment/index')
 */
export default (menus, to = '/') => {
  // 初始路由
  const defRoutes = [
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/login/index'),
      hidden: true
    },
    {
      path: '/',
      component: () => import('@/views/layout/Layout'),
      redirect: '/dashboard',
      name: 'Dashboard',
      children: [
        {
          path: 'dashboard',
          meta: { title: '首页', icon: 'home' },
          component: () => import('@/views/dashboard/index')
        }
      ]
    }
  ]
  // 404路由
  const notFoundRoutes = [
    { path: '/404', name: '404', component: () => import('@/views/404'), hidden: true },
    { path: '*', redirect: '/404', hidden: true }
  ]
  // 初始化路由信息对象
  const menusMap = {}
  menus.map(v => {
    const { path, name, component, redirect, hidden, meta } = v
    // 重新构建路由对象
    const item = {
      path,
      name,
      component: () => import(`@/views/${component}`),
      redirect,
      hidden: JSON.parse(hidden)
    }
    meta.length !== 0 && (item.meta = JSON.parse(meta))
    // 判断是否为根节点
    if (v.pid === 0) {
      menusMap[v.id] = item
    } else {
      !menusMap[v.pid].children && (menusMap[v.pid].children = [])
      menusMap[v.pid].children.push(item)
    }
  })

  // 将生成数组树结构的菜单并拼接404路由
  const routes = Object.values(menusMap).concat(notFoundRoutes)
  // 默认路由拼接生成的路由(注意顺序)
  const integralRoutes = defRoutes.concat(routes)

  Router.options.routes = integralRoutes
  Router.addRoutes(routes)
  Router.push({ path: to })
}

复制代码

写在最后,以上就是我这两天在写权限管理时使用 addRoutes 动态加载路由的方法以及时遇到的一些坑。

第一次写这么长的文章,如果内容有什么不对,望海涵并指出!如果有什么更好的建议也请多多指出!!

如果有喜欢的老铁记得双击加点赞~(开个玩笑)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大连接

大连接

[美] 尼古拉斯•克里斯塔基斯(Nicholas A. Christakis)、[美] 詹姆斯•富勒(James H. Fowler) / 简学 / 中国人民大学出版社 / 2013-1 / 59.90元

[内容简介] 1. 本书是继《六度分隔》之后,社会科学领域最重要的作品。作者发现:相距三度之内是强连接,强连接可以引发行为;相聚超过三度是弱连接,弱连接只能传递信息。 2. 本书讲述了社会网络是如何形成的以及对人类现实行为的影响,如对人类的情绪、亲密关系、健康、经济的运行和政治的影响等,并特别指出,三度影响力(即朋友的朋友的朋友也能影响到你)是社会化网络的强连接原则,决定着社会化网络的......一起来看看 《大连接》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具