该架构是参考公司原架构做了优化,计划慢慢从0开始完全独立自己搭建一个基于springboot的restful服务后台架构,并且完全后端分离。系列文章所涉及的项目源码都放在了个人github上,前台采用vue技术。
这章开始着重讲述前台的搭建和后端的菜单管理,这篇文章需要对 vue-element-admin和shiro比较了解,基础要求略高。
本系列是站在后端Java开发的角度编写,所以对于vue不会详细介绍,着重于框架的使用。
这边采用vue-element-admin框架,该框架是基于vue+elementUI搭建admin管理界面,具体可以点击文中链接。对于非专业前端来说,只要会用这个框架基本就能完成独立的开发,这边也要感谢这位花裤衩大牛能够开源这么好的前端框架。阅读这边文章之前需要先查看花裤衩的文档,学会vue-element-admin基本使用。
对于admin管理系统化来说,主要的几个基础功能是登陆注册,用户管理,菜单管理,角色管理。我们先从简单的需求开始,不必要一步到位完成成熟的一个admin系统(比如部门管理和权限管理等)。这些功能虽然看似简单基础,但开发难度不低而且代码量也不小,而且大部分公司这块逻辑早已写好,无需你过多修改。所以我只选择菜单管理模块来讲解。
vue-element-admin是纯前端的框架,所以完整的路由表是存在前台的,虽然花裤衩大牛也提供了菜单分配的思路,但是站在后台开发来看,路由表肯定是存在后台更安全,前台在登陆后,后台直接传menus即可。
这样我们就需要用到vue的路由动态加载。
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
这边对于vue-element-admin掌握要求比较高,如果没有学习完花裤衩的文档,或者说你完全不会vue,这边就无法进行下去。
先打开src目录下的permission.js,主要修改就是router.beforeEach方法内第一个else后的方法,这边涉及到vue里缓存store知识点。主要思路就是在permission.js中完成对路由的动态加载,从store中取得路由表,那store里的数据又是哪来的呢,在登陆成功后后端会返回userinfo,然后根据其中的roleslist去请求后台getMenus,返回包装好的路由表存到store中。 具体代码看起来非常复杂,但是你只需要知道逻辑就行。
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration
// permission judge function
function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0)
}
const whiteList = ['/login', '/auth-redirect']// no redirect whitelist
router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar
if (getToken()) { // determine if there has token
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取user_info
const roles = res.data.data.roles // note: roles must be a array! such as: ['editor','develop']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.roles, to.meta.roles)) {
next()
} else {
next({ path: '/401', replace: true, query: { noGoBack: true }})
}
// 可删 ↑
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
}
})
router.afterEach(() => {
NProgress.done() // finish progress bar
})
打开src/router/index.js,主要改动是保留静态的公共路由,将动态可配置的删除。保留空的asyncRouterMap。
import Vue from 'vue'
import Router from 'vue-router'
// in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading;
// detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading
Vue.use(Router)
/* Layout */
import Layout from '../views/layout/Layout'
/**
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu, whatever its child routes length
* if not set alwaysShow, only more than one route under the children
* it will becomes nested mode, otherwise not show the root menu
* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb
* name:'router-name' the name is used by (must set!!!)
* meta : {
title: 'title' the name show in submenu and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar,
}
**/
export const constantRouterMap = [
{
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/authredirect'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/errorPage/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/errorPage/401'),
hidden: true
},
{
path: '',
component: Layout,
redirect: 'website',
children: [
{
path: 'website',
component: () => import('@/views/system/website/index'),
name: 'Dashboard',
meta: { title: '网站属性', icon: 'form', noCache: true }
}
]
}
]
export default new Router({
// mode: 'history', //后端支持可开
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
export const asyncRouterMap = []
其实这块的逻辑有些复杂,需要扎实的基础,比如这里我们使用的是shiro的框架。如果不知道shiro的设定的话,这里也无法继续展开来。
熟悉shiro的同学都知道,shiro的主要配置就是realm(登陆验权)和自身shiroFilter(接口的拦截)。
realm里验权需要addrole和addpermission(也可以简单理解成menu),然后我们就可以在具体的controller里的接口上加注解按照role或者permission拦截,同样也可以在shiroFilter设定。
在早期项目不分离的时候,shiro可以说完全独揽控制大权,但是随着项目的前后端分离,分布式系统的流行,shiro的很多功能都已经不再需要了,比如未登陆的时候请求了一些拦截的接口,shiro会帮你重定向到登陆页面(默认login.jsp),分离的项目前端的路由跳转完全由前端管理,重定向反而会遗失了报错信息,我们前端只需要一个返回信息“尚未登陆”,判断是否登陆也只需要shiro返回一个token(shiro中的sessionId),前端的每次请求都是带着token头,后端以此来判断,这些都需要对shiro进行改造。
分布式的项目中,比如springcloud中有自家的gateway组件,配合上jwt模式设计,shiro甚至可以完全被舍弃了。
后期可能会单独开一篇文章详细讲shiro,但是vue这边不会再开篇,花裤衩的教程已经非常详细了。
这边回归正题,我们完成这三个模块的简单搭建。首先我们确定三个模型之间的关系,用户和角色多对多,角色和菜单多对多,用户不直接和菜单有关系。
所以我们需要3张主表和2张关系表:user, role, menu, user_role, role_menu
然后我们完成一个情景需求,根据登陆的这个具体user,返回一个menutree。这个需求非常简单,就是2个关联查询就完成了。但是有几个注意点:
1.menu会重复
2.需要把menus改造成前台vue能解析的路由表(嵌套好的,需要符合vue-element-admin规定)
一个个解决,通常查询是返回的list,是有序可重复,这个问题其实面试题常问,list怎么去重,答案是转成set。但是我们这边需要他有序,因为菜单前端可维护的,需要设置具体的顺序,所以变通转成LinkedHashSet(小课堂:set是根据什么去重的?)。
那怎么包装成vue-element-admin能解析的呢?查看vue-element-admin文档,https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html#配置项
具体每个字段什么意思文档中也都有解释,先学好vue-element-admin非常关键。然后我们照着这个来把我们得到的menusList包装好。这里用到一个知识点:递归 (java基础不多解释)。直接上代码:
@PostMapping("admin/menuByRoles")
public Result getMenu(@RequestBody List roleList) {
//建立左侧菜单树
List menuVOTree = new ArrayList<>();
//获取到所有菜单entity的set
LinkedHashSet menuSet = new LinkedHashSet<>();
for (SysRole role : roleList) {
menuSet.addAll(iSysMenuService.getMenu(role.getId()));
}
//获取到所有菜单VO的list
List menuVOList = new ArrayList<>();
for (SysMenu menu : menuSet) {
SysMenuVO menuVO = new SysMenuVO();
BeanUtils.copyProperties(menu, menuVO);
MetaVO meta = new MetaVO();
if (menu.getMetaTitle() != null) {
meta.setTitle(menu.getMetaTitle());
}
if (menu.getMetaIcon() != null) {
meta.setIcon(menu.getMetaIcon());
}
if (menu.getMetaNocache() != null) {
meta.setNoCache(menu.getMetaNocache());
}
if (menu.getMetaBreadcrumb() != null) {
meta.setBreadcrumb(menu.getMetaBreadcrumb());
}
menuVO.setMeta(meta);
menuVOList.add(menuVO);
}
//遍历volist
for (SysMenuVO menuVO : menuVOList) {
if (menuVO.getParentId() == 0) {
//递归
menuVOTree.add(createTreeChildren(menuVO, menuVOList));
}
}
return new Result(ResultStatusCode.OK, menuVOTree);
}
private SysMenuVO createTreeChildren(SysMenuVO menuVO, List menuVOList) {
for (SysMenuVO menuVO2 : menuVOList) {
if (menuVO2.getParentId().equals(menuVO.getId())) {
if (menuVO.getChildren() == null) {
menuVO.setChildren(new ArrayList<>());
}
menuVO.getChildren().add(createTreeChildren(menuVO2, menuVOList));
}
}
return menuVO;
}
我们这边的菜单是支持无限递归的,意思是菜单的级别可以是n级。menu的菜单是怎么实现层级的呢,答案是设置parentid,0就是顶级菜单,其他子菜单的parentid就是父菜单的id。这段代码比较绕,简单解释就是先找出menuVOList中的顶菜单parentid为0,然后循环顶菜单,继续遍历menuVOList,找到parentid是这个顶菜单的menuVOList,set给menuVO,这里就进入了第一次递归循环。
createTreeChildren这个方法就是递归方法,作用是传一个父菜单,和需要遍历的menuVOList,找出menuVOList中符合的子菜单。然后这个子菜单可以继续调用这个方法,一直到不存在该parentid,所以递归的关键点递归条件和结束条件就理清了。
递归是非常有意思的一个算法,难以理解,但是代码非常简洁,这里其实还可以优化,比如你这个menuVOList其实每次已经被当作父菜单参数的时候,menuVOList可以将其剔除。
至此,根据user返回包装好的menusTree的接口已经开发完成。
1.熟悉vue和vue-element-admin很关键,不做专业前端,掌握起来并不难。
2.掌握shiro,做简单的项目shiro是最适合的。
3.懂得如何建立多对多关系,嵌套查询。
4.学会采用递归算法来造树。