共计 8998 个字符,预计需要花费 23 分钟才能阅读完成。
我的项目基于 vue-element-admin 根底之上进行开发,感激 花裤衩 以及为此我的项目做出奉献的大佬 [社会社会]!
思路
路由数据由后端管制,前端调用接口返回数据;前端拿到数据处理成可应用的路由构造,而后渲染到页面。
- 路由数据从哪来,路由数据不能在后端写死,那样每次增删都会很麻烦,所以须要有专门的零碎去保护,这样能够不便的管制路由的增删;
- 一个专门的零碎只用来保护一个后盾管理系统有点大材小用,所以这个专门的零碎须要能够治理多个利用及对应的路由数据;
- 用户登录后盾管理系统会有本人独自的路由数据,这样才能够不同的人有不同的权限,所以对用户的治理划分了角色,不同的角色能够调配不同的路由权限,不同的角色能够绑定相应的用户,这样不同角色下的用户就能够调配不同的路由权限
- 用户登录后盾管理系统之后会去申请本人的路由数据,后端依据用户的账号绑定的角色返回给前端对应的路由数据
- 前端拿到数据处理成可应用的路由构造,而后渲染到页面
路由数据结构
为了能提供更多的性能,后端返回的数据结构是在原来菜单构造根底之上有所改变的。
原路由数据结构:
{
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 字段,设置子级页面 title
let 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 router
resetRouter()
router.addRoutes(addRoutes)
至此对路由的解决完结。
解决路由这一步是放在 router.beforeEach
路由全局前置守卫里的,联合原有的逻辑进行了一些判断解决。
因为路由数据是由后端数据处理失去的,只蕴含和挂载了以后用户能有权限拜访的局部,没有权限拜访的路由就没有被 router.addRoutes
增加,所以并没有在路由守卫里对每次路由的跳转都进行校验。