博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
前端权限管理之 addRoutes 动态加载路由踩坑
阅读量:7126 次
发布时间:2019-06-28

本文共 9119 字,大约阅读时间需要 30 分钟。

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

  1. 后端生成当前用户相应的路由后由前端(用 Vue Router 提供的API)addRoutes 动态加载路由。
  2. 前端写好所有的路由,后端返回当前用户的角色,然后根据事先约定好的每个角色拥有哪些路由对角色的路由进行分配。

两种方法的不同

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

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

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

addRoutes 需要的数据格式

router.addRoutes

函数签名:

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

动态添加更多的路由规则。参数必须是一个符合 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)的 ,具体可以参考大佬的代码,这里也不再赘述了。

如果坚持看到了这里,那么恭喜你,基本就可以通过 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 动态加载路由的方法以及时遇到的一些坑。

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

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

转载于:https://juejin.im/post/5c83ccb75188257e342db5c9

你可能感兴趣的文章
Popular Cows//强连通分支Kosaraju加缩点
查看>>
史上最详细“截图”搭建Hexo博客并部署到Github
查看>>
关于nginx的limit模块
查看>>
使用伴生对象创建计数器工具类
查看>>
leetcode------Rotate Array
查看>>
省级三连动(二)
查看>>
获取China大陆IP段的范围
查看>>
触发器
查看>>
Java开发者必读的5本最佳Hibernate书籍
查看>>
nginx设置目录浏览及解决中文乱码问题
查看>>
Linux多线程编程(不限Linux)
查看>>
Object中的方法
查看>>
swift -- 集合
查看>>
Oracle跟踪文件
查看>>
程序员学炒股(7) 股指期货收盘价对第二天开盘价有影响吗?
查看>>
关于离线缓存webView的新方法NSURLProtocol
查看>>
unittest详解(六) 断言
查看>>
F#与数学(I) – PowerPack中的数字类型
查看>>
Window Phone上的F# - 图形计算器
查看>>
lower_bound()返回值
查看>>