我的项目基于 vue-element-admin 根底之上进行开发,感激 花裤衩 以及为此我的项目做出奉献的大佬 [社会社会]!

思路

路由数据由后端管制,前端调用接口返回数据;前端拿到数据处理成可应用的路由构造,而后渲染到页面。

  1. 路由数据从哪来,路由数据不能在后端写死,那样每次增删都会很麻烦,所以须要有专门的零碎去保护,这样能够不便的管制路由的增删;
  2. 一个专门的零碎只用来保护一个后盾管理系统有点大材小用,所以这个专门的零碎须要能够治理多个利用及对应的路由数据;
  3. 用户登录后盾管理系统会有本人独自的路由数据,这样才能够不同的人有不同的权限,所以对用户的治理划分了角色,不同的角色能够调配不同的路由权限,不同的角色能够绑定相应的用户,这样不同角色下的用户就能够调配不同的路由权限
  4. 用户登录后盾管理系统之后会去申请本人的路由数据,后端依据用户的账号绑定的角色返回给前端对应的路由数据
  5. 前端拿到数据处理成可应用的路由构造,而后渲染到页面

路由数据结构

为了能提供更多的性能,后端返回的数据结构是在原来菜单构造根底之上有所改变的。

原路由数据结构:

{  path: '/example',  component: Layout,  redirect: '/example/list',  name: 'Example',  meta: {    title: 'Example',    icon: 'el-icon-s-help'  },  children: [    {      path: 'create',      component: () => import('@/views/example/create'),      name: 'CreateArticle',      meta: { title: 'Create Article', icon: 'edit' }    },    {      path: 'edit/:id(\\d+)',      component: () => import('@/views/example/edit'),      name: 'EditArticle',      meta: { title: 'Edit Article', noCache: true, activeMenu: '/example/list' },      hidden: true    }  ]}

后端返回数据结构:

{  "id": "",  "type": 0, // 用来标注以后层级的类型 0:头部菜单;1:侧边目录;2:侧边菜单;3:页面按钮  "name": "商家治理", // 绝对于 meta 外面的 title  "icon": "", // 绝对于 meta 外面的 icon  "component": "", // 寄存组件关键字,path、name 应用此字段  "permissionFlag": "storeManagement:head", // 权限标识  "sort": 1,  "children": [    {      "id": "",      "type": 1,      "name": "商家治理",      "icon": "",      "component": "",      "permissionFlag": "storeManage:menu",      "sort": 1,      "children": [        {          "id": "",          "type": 2,          "name": "商家治理",          "icon": "",          "component": "storeManage",          "permissionFlag": "storeManage:page",          "sort": 1,          "children": [            {              "id": "",              "type": 3,              "name": "新增",              "icon": "",              "component": "storeManageDetail",              "permissionFlag": "storeManageDetail:add",              "sort": null,              "children": null            }          ]        }      ]    }  ]}

路由数据采纳多级嵌套构造,根本能够满足需要。

解决路由数据

拿到接口返回的数据之后还不能间接应用,须要通过一些解决。

合并开发的长期路由数据

在本地的开发,新增页面时,为了不便开发,获取到后端返回的数据之后用了一个办法来合并数据:

let temporaryMenuArray = [] // 长期路由数组function getNewPermissionTree(temporaryMenuArray, userPermissionMenu) {  if (temporaryMenuArray && temporaryMenuArray.length > 0) {    temporaryMenuArray.forEach((tempItem) => {      if (!userPermissionMenu.some((it) => it.permissionFlag === tempItem.permissionFlag)) {        userPermissionMenu.push(tempItem)      } else {        userPermissionMenu = userPermissionMenu.map((item) => {          if (item.permissionFlag === tempItem.permissionFlag) {            item.children = getNewPermissionTree(tempItem.children || [], item.children || [])          }          return item        })      }    })  }  return userPermissionMenu}export const setTemporaryMenu = function (userPermissionMenu) {  return getNewPermissionTree(temporaryMenuArray, userPermissionMenu)}

temporaryMenuArray 长期路由数组的构造须要跟后端返回的数据结构统一。

temporaryMenuArray = [  {    "id": "",    "type": 0, // 用来标注以后层级的类型    "name": "商家治理", // 绝对于 meta 外面的 title    "icon": "", // 绝对于 meta 外面的 icon    "component": "", // 寄存组件关键字,path、name 应用此字段    "permissionFlag": "storeManagement:head", // 权限标识    "sort": 1,    "children": [      {        "id": "",        "type": 1,        "name": "商家治理",        "icon": "",        "component": "",        "permissionFlag": "storeManage:menu",        "sort": 1,        "children": [          {            "id": "",            "type": 2,            "name": "商家治理",            "icon": "",            "component": "storeManage",            "permissionFlag": "storeManage:page",            "sort": 1,            "children": [+ --          {+ --            "id": "",+ --            "type": 3,+ --            "name": "查看",+ --            "icon": "",+ --            "component": "storeManageDetail",+ --            "permissionFlag": "storeManageDetail:view",+ --            "sort": null,+ --            "children": null+ --          }            ]          }        ]      },+ --  {+ --    "id": "",+ --    "type": 1,+ --    "name": "商品治理",+ --    "icon": "",+ --    "component": "",+ --    "permissionFlag": "goodsManage:menu",+ --    "sort": 1,+ --    "children": [+ --      {+ --        "id": "",+ --        "type": 2,+ --        "name": "商品治理",+ --        "icon": "",+ --        "component": "goodsManage",+ --        "permissionFlag": "goodsManage:page",+ --        "sort": 1,+ --        "children": [+ --          {+ --            "id": "",+ --            "type": 3,+ --            "name": "新增",+ --            "icon": "",+ --            "component": "goodsManageDetail",+ --            "permissionFlag": "goodsManageDetail:add",+ --            "sort": null,+ --            "children": null+ --          }+ --        ]+ --      }+ --    ]+ --  }    ]  }]
+ -- 局部为为新增数据。

应用 setTemporaryMenu:

import { setTemporaryMenu } from '@/utils/set-temporary-menu'// userPermissionMenu 为从接口获取权限菜单数据// 把接口返回的菜单数据跟长期设置的菜单数据合并userPermissionMenu = setTemporaryMenu(userPermissionMenu)

合并完长期的路由之后就开始进行路由的解决了。

解决路由模块

从下面后端返回的数据结构中能够看出,曾经和须要解决成的路由很像了,但还是有一些数据没有必要在专门的零碎那里保护,所以须要一些其余数据的解决:

import { routesComponents } from '@/router/routes'// 用于设置 headerMenu 字段,标注菜单属于哪一个头部一级菜单let headerMenu = ''// 用于设置 activeMenu 字段,能够使跳转子级页面时的父级页面高亮let activeMenu = ''// 用于设置 parentName 字段,设置子级页面 titlelet parentName = ''// 用于设置 parentPath 字段,设置嵌套目录的门路let parentPath = ''// 用于收集路由,避免设置反复路由let routesComponentTemp = new Set()/** * 收集以后用户能够应用的路由模块 * @param {*} userPermissionMenu 接口返回的权限菜单数据 */function gatherUserCanUseRouteModules(userPermissionMenu) {  let routersTemp = []  userPermissionMenu.forEach((route) => {    let routeTemp = {}    let { children: userRouteChildren = [], name, icon, ...userRouteOtherData } = route    let permissionFlagArr = route.permissionFlag.split(':')    let flag = permissionFlagArr[0]    // !routersTemp.some(it => it.name === flag) 避免 同一个父级下有反复路由    // !routesComponentTemp.has(route.component) 避免 不同层级下有反复路由    if (!routersTemp.some((it) => it.name === flag) && !routesComponentTemp.has(route.component)) {      if (route.type === 0) {        headerMenu = route.permissionFlag        routeTemp.meta = { title: name }        routeTemp.isHeaderMenu = true      }      if (route.type === 1) {        routeTemp.alwaysShow = false        // route.type 为 1 时,为目录层级,component 默认为 Layout。没有考录多层目录状况。        // 多层目录状况下能够思考在新建菜单时用 权限标识(permissionFlag) 字段辨别。例如        // 例如:权限标识(permissionFlag): "storeManage:menu:2"        // :2 能够辨别出此时的目录为嵌套目录,则 component 须要设置为 flag(此路由也须要在 @/router/routes 下新建过渡页面)        let isNestedMenu = /\d/.test(permissionFlagArr[permissionFlagArr.length - 1])        parentPath = isNestedMenu ? parentPath : flag        routeTemp.component = isNestedMenu ? routesComponents[flag] : routesComponents['Layout']        routeTemp.headerMenu = headerMenu        routeTemp.meta = { icon: icon, title: name }        routeTemp.path = isNestedMenu ? flag : `/${flag}`        let firstPath = isNestedMenu ? `/${parentPath}` : ''        routeTemp.redirect = `${firstPath}/${flag}/${userRouteChildren[0].permissionFlag.split(':')[0]}`        activeMenu = routeTemp.redirect      }      if (route.type === 2) {        routeTemp.path = flag        routeTemp.component = routesComponents[route.component]        // route.type 为 2 时,为列表页面 默认 noCache:false 缓存页面        // 当 权限标识(permissionFlag) 蕴含 :keepAlive 时 noCache 会被设置为 true 不缓存页面        let noCache = permissionFlagArr[permissionFlagArr.length - 1] === 'keepAlive' ? true : false        routeTemp.meta = { title: name, isBack: false, isEdit: false, noCache }        parentName = name        routesComponentTemp.add(route.component)      }      if (route.type === 3 && route.component) {        routeTemp.path = flag        routeTemp.component = routesComponents[route.component]        // route.type 为 3 时,为按钮跳转页面 默认 noCache:true 不缓存页面        // 当 权限标识(permissionFlag) 蕴含 :keepAlive 时 noCache 会被设置为 false 缓存页面        let noCache = permissionFlagArr[permissionFlagArr.length - 1] === 'keepAlive' ? false : true        name = ['新建', '新增', '查看', '编辑'].includes(name) ? '详情' : name        routeTemp.meta = { title: `${parentName} - ${name}`, noCache, activeMenu: activeMenu }        routeTemp.hidden = true        routesComponentTemp.add(route.component)      }      // 以后层级有子级,则去收集子级      let userRouteChildrenTemp = []      if (userRouteChildren && userRouteChildren.length) {        userRouteChildrenTemp = gatherUserCanUseRouteModules(userRouteChildren)      }      routeTemp = { ...userRouteOtherData, ...routeTemp }      // 当 route.type 为 0、1 时,name 设置为 permissionFlag 避免反复      routeTemp.name = [0, 1].includes(route.type) ? route.permissionFlag : flag      if (route.type === 0 || route.type === 1 || (route.type === 3 && route.component)) {        routersTemp.push({ ...routeTemp, children: userRouteChildrenTemp })      } else if (route.type === 2) {        // 当 route.type 为 2 使其子级跟其平级,不必再嵌套页面        routersTemp.push(routeTemp, ...userRouteChildrenTemp)      }    }  })  return routersTemp}// 收集以后用户能够应用的路由模块const userCanUseRouteModules = gatherUserCanUseRouteModules(userPermissionMenu)

routesComponents 为一个手动保护的页面路由文件:

// 写成 ...{} 格局是为了便于辨别/* Layout */const Layout = {  Layout: () => import('@/layout'),}/* 商家治理 storeManagement */const storeManagement = {  // 商家治理  ...{    storeManage: () => import('@/views/store/storeManage'),    storeManageDetail: () => import('@/views/store/storeManageDetail'),  },  // 商品治理  ...{    goodsManage: () => import('@/views/goods/goodsManage'),    goodsManageDetail: () => import('@/views/goods/goodsManageDetail'),  },}export const routesComponents = {  ...Layout,  ...storeManagement,}

失去的 userCanUseRouteModules 构造如下:

[  {    "id": "",    "type": 0,    "component": "",    "path": null,    "permissionFlag": "storeManagement:head",    "sort": 1,    "meta": {      "title": "商家治理"    },    "isHeaderMenu": true,    "name": "storeManagement:head",    "children": [      {        "id": "",        "type": 1,        "component": "",        "path": "/storeManage",        "permissionFlag": "storeManage:menu",        "sort": 1,        "alwaysShow": false,        "headerMenu": "storeManagement:head",        "meta": {          "icon": "",          "title": "商家治理"        },        "redirect": "/storeManage/storeManage",        "name": "storeManage:menu",        "children": [          {            "id": "",            "type": 2,            "component": "",            "path": "storeManage",            "permissionFlag": "storeManage:page",            "sort": 1,            "meta": {              "title": "商家治理",              "isBack": false,              "isEdit": false,              "noCache": false            },            "name": "storeManage"          },          {            "id": "",            "type": 3,            "component": "",            "path": "storeManageDetail",            "permissionFlag": "storeManageDetail:view",            "sort": null,            "meta": {              "title": "商家治理 - 详情",              "noCache": true,              "activeMenu": "/storeManage/storeManage"            },            "hidden": true,            "name": "storeManageDetail",            "children": []          }        ]      }    ]  }]

下面数据中的 type 为2、3层级的 component 曾经被替换成对应页面的路由模块了。

获取须要增加的路由

失去 userCanUseRouteModules 之后,其中 name 为 storeManagement:head 的那一层数据是供头部菜单应用的,其 children 的数据才是须要增加的路由数据:

import { asyncRoutes } from '@/router'// 须要增加的的路由const addRoutes = [...userCanUseRouteModules.reduce((prev, curr) => [...prev, ...curr.children], []), ...asyncRoutes]

此时的 asyncRoutes 只放了默认匹配的路由,这个是须要放在所有路由的最初的:

export const asyncRoutes = [  // 404 page must be placed at the end !!!  { path: '*', redirect: '/404', hidden: true },]

应用 router.addRoutes 增加路由

import router, { resetRouter } from '@/router'// reset routerresetRouter()router.addRoutes(addRoutes)

至此对路由的解决完结。

解决路由这一步是放在 router.beforeEach 路由全局前置守卫里的,联合原有的逻辑进行了一些判断解决。

因为路由数据是由后端数据处理失去的,只蕴含和挂载了以后用户能有权限拜访的局部,没有权限拜访的路由就没有被 router.addRoutes 增加,所以并没有在路由守卫里对每次路由的跳转都进行校验。