关于后台管理系统的路由,想花一点时间,完全的整理一份实现动态路由的点点滴滴。前端
首先声明,这篇文章是基于花裤衩大神的《手摸手,带你用vue撸后台》,在他项目的基础上,帮助想要实现动态路由的小伙伴,来写的一篇使用笔记。vue
咱们在开发后台管理系统的过程当中,会有不一样的人来操做系统,有admin(管理员)、superAdmin(超管),还会有各类运营人员、财务人员。为了区别这些人员,咱们会给不一样的人分配不同的角色,从而来展现不一样的菜单,这个就必需要经过动态路由来实现。vue-router
简单聊一下两种方式的优点,毕竟若是你历来没作过,说再多也看不明白,仍是得看代码vuex
一、不用后端帮助,路由表维护在前端
二、逻辑相对比较简单,比较容易上手
复制代码
一、相对更安全一点
二、路由表维护在数据库
复制代码
花裤衩大神的方案是前端控制,他的核心是经过路由的meta属性,经过role来控制路由的加载。具体的实现方案:数据库
一、根据登陆用户的帐号,返回前端用户的角色
二、前端根据用户的角色,跟路由表的meta.role进行匹配
三、讲匹配到的路由造成可访问路由
复制代码
具体的代码逻辑:segmentfault
一、把静态路由和动态路由分别写在router.js
二、在vuex维护一个state,经过配角色来控制菜单显不显示
三、新建一个路由守卫函数,能够在main.js,也能够抽离出来一个文件
四、侧边栏的能够从vuex里面取数据来进行渲染
复制代码
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/layout'
// constantRoutes 静态路由,主要是登陆页、404页等不须要动态的路由
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
}
]
// asyncRoutes 动态路由
export const asyncRoutes = [
{
path: '/permission',
component: Layout,
redirect: '/permission/page',
alwaysShow: true,
name: 'Permission',
meta: {
title: 'Permission',
icon: 'lock',
// 核心代码,能够经过配的角色来进行遍历,从而是否展现
// 这个意思就是admin、editor这两个角色,这个菜单是能够显示
roles: ['admin', 'editor']
},
children: [
{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'PagePermission',
meta: {
title: 'Page Permission',
// 这个意思就是只有admin能展现
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
复制代码
import { asyncRoutes, constantRoutes } from '@/router'
// 这个方法是用来把角色和route.meta.role来进行匹配
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) => {
// 这个地方维护了两个状态一个是addRouters,一个是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)
}
// 把匹配完有权限的路由给set到vuex里面
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
复制代码
这里面的代码主要是控制路由跳转以前,先查一下有哪些可访问的路由,登陆之后跳转的逻辑能够在这个地方写后端
// permission.js
router.beforeEach((to, from, next) => {
if (store.getters.token) { // 判断是否有token
if (to.path === '/login') {
next({ path: '/' });
} else {
// 判断当前用户是否已拉取完user_info信息
if (store.getters.roles.length === 0) {
store.dispatch('GetInfo').then(res => { // 拉取info
const roles = res.data.role;
// 把获取到的role传进去进行匹配,生成能够访问的路由
store.dispatch('GenerateRoutes', { roles }).then(() => {
// 动态添加可访问路由表(核心代码,没有它啥也干不了)
router.addRoutes(store.getters.addRouters)
// hack方法 确保addRoutes已完成
next({ ...to, replace: true })
})
}).catch(err => {
console.log(err);
});
} else {
next() //当有用户权限的时候,说明全部可访问路由已生成 如访问没权限的全面会自动进入404页面
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登陆白名单,直接进入
next();
} else {
next('/login'); // 不然所有重定向到登陆页
}
}
})
复制代码
核心代码是从router取能够用的路由对象,来进行侧边栏的渲染,不论是前端动态加载仍是后端动态加载路由,这个代码都是同样的数组
<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里面
五、侧边栏也是从路由取数据进行渲染
复制代码
由于前段控制和后端控制,后面的流程大部分都是同样的,因此这个地方只看看前面不同的流程:
GenerateRoutes({ commit }, data) {
return new Promise((resolve, reject) => {
getRoute(data).then(res => {
// 将获取到的数据进行一个转换,而后存到vuex里
const accessedRouters = arrayToMenu(res.data)
accessedRouters.concat([{ path: '*', redirect: '/404', hidden: true }])
commit('SET_ROUTERS', accessedRouters)
resolve()
}).catch(error => {
reject(error)
})
})
}
复制代码
咱们知道vue的router规定的数据结构是这样的:
{
path: '/form',
component: Layout,
children: [
{
path: 'index',
name: 'Form',
component: () => import('@/views/form/index'),
meta: { title: 'Form', icon: 'form' }
}
]
}
复制代码
因此,一级菜单有几个参数必需要有:id、path、name、component、title,二级菜单children是一个数组,是子父级的关系,因此能够给加一个fid或者parentId,来进行匹配,后面写转换方法的时候会详细解释,数据格式大概就是这样:
// 一级菜单
// parentId为0的就能够当作一级菜单,id最好是能够选4位数,至于为何等你开发项目的时候就知道了
{
id: 1300
parentId: 0
title: "企业管理"
path: "/enterprise"
hidden: false
component: null
hidden: false
name: "enterprise"
},
// 二级菜单
// parentId不为0的,就能够拿parentId跟一级菜单的id去匹配,匹配上的就push到children里面
{
id: 1307
parentId: 1300
title: "商户信息"
hidden: false
path: "merchantInfo"
component: "enterprise/merchantInfo" // 要跟本地的文件地址匹配上
hidden: false
name: "merchantInfo"
}
复制代码
刚才获取到的数据没法直接转成router进行渲染,须要一个arrayToMenu的方法,刚才也说了一些思路,下面就一块儿分析下这个方法:
export function arrayToMenu(array) {
const nodes = []
// 获取顶级节点
for (let i = 0; i < array.length; i++) {
const row = array[i]
// 这个exists方法就是判断下有没有子级
if (!exists(array, row.parentId)) {
nodes.push({
path: row.path, // 路由地址
hidden: row.hidden, // 所有携程true就行,若是后端没配
component: Layout, // 通常就是匹配你文件的component
name: row.name, // 路由名称
meta: { title: row.title, icon: row.name }, // title就是显示的名字
id: row.id, // 路由的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]
// parentId等于哪一个父级的id,就push到哪一个
if (row.parentId === node.id) {
const child = {
path: row.path,
name: row.name,
hidden: row.hidden,
// 核心代码,由于二级路由的component是须要匹配页面的
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
}
复制代码
侧边栏的代码跟静态的代码是同样的,就再也不说一遍了
动态路由到底是前端控制好,仍是后端控制好?只能说各有各的优点,毕竟业务场景也不同,你们能够动手来试一下。代码还有不少值得优化的地方,欢迎各位大神批评指正。
我的公众号:小Jerry有话说
欢迎关注个人我的公众号,一块儿探讨有趣的前端知识。