1、背景介绍:最近因为公司的项目,对一个后台管理系统进行前端重构,原项目是SSM架构的前后端没有分离,前端用JSP和jQuery来写的,在完成第一期之后,我强烈要求前后端分离,并使用vue来组件化开发。所以经过几天的摸索完成了项目的整体搭建和基础建设。
2、项目要求:因为原先的项目的菜单都是后台来动态生成的,所以用vue来写也要从后台获取路由表,前端再处理成我们想要的路由格式,再完成侧边栏菜单;并且前端页面里有一个模块专门来管理生成菜单和权限(也就是前端自定义一些菜单信息传给后端,后端把数据存到数据库,并根据角色权限处理完数据再返回路由表给前端,前端就可以动态生成侧边栏菜单了)。
首先本项目是基于vue-element-admin的后台管理模板( vue-element-admin),这个模板很多功能都有现成的,很方便,拉取下来之后就可以基于这个进行开发了,这个模板有很详细的文档说明( vue-element-admin使用文档),里面也有动态路由的集成方案,本项目选择的就是路由全部通过后端来生成,前端来自定义需要的路由。
步骤如下:
1、用户登录成功之后(登录的功能,模板里也写的很好,换成公司的登录接口就可以直接用),再根据用户名获取用户的权限,然后根据用户权限获取后端的路由数据:
1)权限处理和生成动态路由:src/permission.js文件
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // 进度条
import 'nprogress/nprogress.css' // 进度条样式
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
// import { getRoutes } from './api/role'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/auth-redirect'] // 没有重定向白名单
router.beforeEach(async(to, from, next) => {
// 开始进度条
NProgress.start()
// 设置页面标题
document.title = getPageTitle(to.meta.title)
// 确定用户是否已登录,获取token
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 如果已登录,则重定向到主页
next({ path: '/' })
NProgress.done()
} else {
// 确定用户是否通过getInfo获得了他的权限角色
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户权限信息
const { roles } = await store.dispatch('user/getInfo')
// 根据角色生成可访问路由映射
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// console.log(accessRoutes)
// 动态添加可访问路由
router.options.routes = router.options.routes.concat(accessRoutes)
router.addRoutes(accessRoutes)
// hack方法 以确保addRoutes是完整的
next({ ...to, replace: true })
// console.log(router)
} catch (error) {
// 删除令牌,进入登录页面重新登录
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 没有令牌*/
if (whiteList.indexOf(to.path) !== -1) {
// 去往免费的登录白名单
next()
} else {
// 没有访问权限的其他页面被重定向到登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
2)获取用户权限的代码:src/store/modules/user.js
// 获取用户权限信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.code).then(response => {
const { data } = response
// console.log(data)
if (!data) {
reject('验证失败,请重新登录.')
}
let roles = []
roles = roles.concat(data.usrGroupcode)
const name = data.userName
// // 角色必须是非空数组
if (!roles || roles.length <= 0) {
reject('getInfo: 角色必须是非空数组!')
}
const avatar = data.userCode
commit('SET_NAME', name)
commit('SET_ROLES', roles)
commit('SET_CODE', avatar)
const users = {
roles: roles,
introduction: '',
code: avatar,
name: name
}
// console.log(users)
resolve(users)
}).catch(error => {
reject(error)
})
})
},
2、处理后端返回的路由信息并存到vuex里:src/store/modules/permission.js
import { constantRoutes } from '@/router'
import { getMenu } from '../../api/role'
import Layout from '@/layout'
/**
* 将后端content字段转成组件对象
*/
function _import(file) {
return () => import(`@/views/${file}/index.vue`)
}
/**
* 通过递归过滤异步路由表
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes) {
const res = []
// eslint-disable-next-line no-empty
for (let i = 0; i < routes.length; i++) {
res.push({
path: routes[i].content === 'Layout' ? `/${routes[i].path}` : routes[i].path,
component: routes[i].content === 'Layout' ? Layout : _import(routes[i].content),
name: routes[i].path,
meta: {
title: routes[i].menuname,
icon: routes[i].icon
},
children: routes[i].children && routes[i].children.length ? filterAsyncRoutes(routes[i].children) : []
})
}
return res
}
const state = {
routes: constantRoutes,
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise((resolve, reject) => {
let accessedRoutes
let asyncRoutes = []
getMenu(roles[0]).then(res => {
if (res.msg === 'SUCCESS') {
asyncRoutes = res.data
} else {
asyncRoutes = []
}
// console.log(asyncRoutes)
accessedRoutes = filterAsyncRoutes(asyncRoutes)
// 最后添加
const unfound = { path: '*', redirect: '/404', hidden: true }
accessedRoutes.push(unfound)
// console.log(accessedRoutes)
// console.log('store:accessedRoutes')
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
}).catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
基础路由和统一路由:src/router/index.js
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: '/auth-redirect',
component: () => import('@/views/login/auth-redirect'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error-page/401'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: 'home',
meta: { title: '首页', affix: true },
hidden: true,
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard'),
hidden: true
}
]
}
]
3、然后再通过递归生成侧边栏菜单:src/layout
这个时候打印路由信息是可以得到正常的路由表信息;根据路由表生成侧边栏:
this.$router.options.routes 可以获取路由表信息,具体逻辑vue-element-admin模板里有现成的,这里就不解释了。
最后说一下后端返回的数据结构:
[{
applicationid: 1035
children: [ //子节点,结构和当前一样
0:{
applicationid: 1035
children: [
0: {
applicationid: 1035
children: null
content: "system/system-management/menu"
formid: 101100
formtype: 1
icon: ""
inactived: 1
memo: "EAM"
menuid: 101100
menuname: "菜单管理"
parentmenuid: 101000
parentrowid: "01KBGO"
path: "menu"
roles: ""
rowid: "01KBGP"
seq: 101100
treecontrol: "01KBGP01KBGO01KBG9"
}
]
content: "system/system-management"
formid: 101000
formtype: 1
icon: ""
inactived: 1
memo: "EAM"
menuid: 101000
menuname: "系统设置"
parentmenuid: 100000
parentrowid: "01KBG9"
path: "system-management"
roles: ""
rowid: "01KBGO"
seq: 101000
treecontrol: "01KBGO01KBG9"
}
]
content: "Layout" //对应组件的名称
formid: 100000
formtype: 1
icon: "" //菜单的图标
inactived: 1 //是否启用
memo: "EAM"
menuid: 100000 //菜单的id
menuname: "系统管理" //菜单的名称
parentmenuid: null //父级菜单id
parentrowid: ""
path: "system" //路由
roles: ""
rowid: "01KBG9"
seq: 100000 //排序
treecontrol: "01KBG9"
}]