为什么要实现动态路由?
我们在开发后台管理系统的过程中,会有不同的人来操作系统,有admin(管理员)、superAdmin(超管),还会有各种运营人员、财务人员。为了区别这些人员,我们会给不同的人分配不一样的角色,从而来展示不同的菜单,这个就必须要通过动态路由来实现。
主流的实现方式
简单聊一下两种方式的优势,毕竟如果你从来没做过,说再多也看不明白,还是得看代码
前端控制
- 不用后端帮助,路由表维护在前端
- 逻辑相对比较简单,比较容易上手
后端控制
- 相对更安全一点
- 路由表维护在数据库
一、前端控制
思路:在路由配置里,通过meta属性,扩展权限相关的字段,在路由守卫里通过判断这个权限标识,实现路由的动态增加,及页面跳转;如:我们增加一个role字段来控制角色
具体方案:
1、根据登录用户的账号,返回前端用户的角色
2、前端根据用户的角色,跟路由表的meta.role进行匹配
3、讲匹配到的路由形成可访问路由
核心代码逻辑
1、在router.js文件(把静态路由和动态路由分别写在router.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| import Vue from 'vue' import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/layout'
export const constantRoutes = [ { path: '/redirect', component: Layout, hidden: true, children: [ { path: '/redirect/:path*', component: () => import('@/views/redirect/index') } ] }, { path: '/login', component: () => import('@/views/login/index'), hidden: true }, { path: '/404', component: () => import('@/views/error-page/404'), hidden: true }, { path: '/401', component: () => import('@/views/error-page/401'), hidden: true } ]
export const asyncRoutes = [ { path: '/permission', component: Layout, redirect: '/permission/page', alwaysShow: true, name: 'Permission', meta: { title: 'Permission', icon: 'lock', roles: ['admin', 'editor'] }, children: [ { path: 'page', component: () => import('@/views/permission/page'), name: 'PagePermission', meta: { title: 'Page Permission', roles: ['admin'] } } ] } ]
const createRouter = () => new Router({ scrollBehavior: () => ({ y: 0 }), routes: constantRoutes })
const router = createRouter()
export function resetRouter() { const newRouter = createRouter() router.matcher = newRouter.matcher }
export default router
|
2、store/permission.js(在vuex维护一个state,通过配角色来控制菜单显不显示)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| import { asyncRoutes, constantRoutes } from '@/router'
function hasPermission(roles, route) { if (route.meta && route.meta.roles) { return roles.some(role => route.meta.roles.includes(role)) } else { return true } }
export function filterAsyncRoutes(routes, roles) { const res = []
routes.forEach(route => { const tmp = { ...route } if (hasPermission(roles, tmp)) { if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, roles) } res.push(tmp) } })
return res }
const state = { routes: [], addRoutes: [] }
const mutations = { SET_ROUTES: (state, routes) => { state.addRoutes = routes state.routes = constantRoutes.concat(routes) } }
const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { let accessedRoutes if (roles.includes('admin')) { accessedRoutes = asyncRoutes || [] } else { accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) } commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) }) } }
export default { namespaced: true, state, mutations, actions }
|
3、src/permission.js(新建一个路由守卫函数,可以在main.js,也可以抽离出来一个文件)
这里面的代码主要是控制路由跳转之前,先查一下有哪些可访问的路由,登录以后跳转的逻辑可以在这个地方写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| router.beforeEach((to, from, next) => { if (store.getters.token) { if (to.path === '/login') { next({ path: '/' }); } else { if (store.getters.roles.length === 0) { store.dispatch('GetInfo').then(res => { const roles = res.data.role; store.dispatch('GenerateRoutes', { roles }).then(() => { router.addRoutes(store.getters.addRouters) next({ ...to, replace: true }) }) }).catch(err => { console.log(err); }); } else { next() } } } else { if (whiteList.indexOf(to.path) !== -1) { next(); } else { next('/login'); } } })
|
4、侧边栏的可以从vuex里面取数据来进行渲染
核心代码是从router取可以用的路由对象,来进行侧边栏的渲染,不管是前端动态加载还是后端动态加载路由,这个代码都是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="variables.menuBg" :text-color="variables.menuText" :unique-opened="false" :active-text-color="variables.menuActiveText" :collapse-transition="false" mode="vertical" > // 把取到的路由进行循环作为参数传给子组件 <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> </el-menu> // 获取有权限的路由 routes() { return this.$router.options.routes }
<template slot="title"> <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> </template> <sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" />
props: { // route object item: { type: Object, required: true }, isNest: { type: Boolean, default: false }, basePath: { type: String, default: '' } }
|
前端控制路由,逻辑相对简单,后端只需要存这个用户的角色就可以了,前端拿用户的角色进行匹配。但是如果新增角色,就会非常痛苦,每一个都要加。
二、后端控制路由
后端控制大致思路是:路由配置放在数据库表里,用户登录成功后,根据角色权限,把有权限的菜单传给前端,前端格式化成页面路由识别的结构,再渲染到页面菜单上;
- 用户登录以后,后端根据该用户的角色,直接生成可访问的路由数据,注意这个地方是数据
- 前端根据后端返回的路由数据,转成自己需要的路由结构
具体逻辑:
- router.js里面只放一些静态的路由,login、404之类
- 整理一份数据结构,存到表里
- 从后端获取路由数据,写一个数据转换的方法,讲数据转成可访问的路由
- 也是维护一个vuex状态,将转换好的路由存到vuex里面
- 侧边栏也是从路由取数据进行渲染
因为前段控制和后端控制,后面的流程大部分都是一样的,所以这个地方只看看前面不一样的流程:
1、store/permission.js,在vuex里面发送请求获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13
| GenerateRoutes({ commit }, data) { return new Promise((resolve, reject) => { getRoute(data).then(res => { const accessedRouters = arrayToMenu(res.data) accessedRouters.concat([{ path: '*', redirect: '/404', hidden: true }]) commit('SET_ROUTERS', accessedRouters) resolve() }).catch(error => { reject(error) }) }) }
|
2、整理一份数据结构,存到表里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| { path: '/form', component: Layout, children: [ { path: 'index', name: 'Form', component: () => import('@/views/form/index'), meta: { title: 'Form', icon: 'form' } } ] }
{ id: 1300 parentId: 0 title: "菜单管理" path: "/menu" hidden: false component: null hidden: false name: "menu" },
{ id: 1307 parentId: 1300 title: "子菜单" hidden: false path: "menuItem" component: "menu/menuItem" hidden: false name: "menuItem" }
|
3、写一个转化方法,把获取到的数据转换成router结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| export function arrayToMenu(array) { const nodes = [] for (let i = 0; i < array.length; i++) { const row = array[i] if (!exists(array, row.parentId)) { nodes.push({ path: row.path, hidden: row.hidden, component: Layout, name: row.name, meta: { title: row.title, icon: row.name }, id: row.id, redirect: 'noredirect' }) } } const toDo = Array.from(nodes) while (toDo.length) { const node = toDo.shift() for (let i = 0; i < array.length; i++) { const row = array[i] if (row.parentId === node.id) { const child = { path: row.path, name: row.name, hidden: row.hidden, component: require('@/views/' + row.component + '/index.vue'), meta: { title: row.title, icon: row.name }, id: row.id } if (node.children) { node.children.push(child) } else { node.children = [child] } toDo.push(child) } } } return nodes }
function exists(rows, parentId) { for (let i = 0; i < rows.length; i++) { if (rows[i].id === parentId) return true } return false }
|