Springboot单体架构搭建|第四章 前端框架选择和菜单管理

前言

该架构是参考公司原架构做了优化,计划慢慢从0开始完全独立自己搭建一个基于springboot的restful服务后台架构,并且完全后端分离。系列文章所涉及的项目源码都放在了个人github上,前台采用vue技术。
这章开始着重讲述前台的搭建和后端的菜单管理,这篇文章需要对 vue-element-admin和shiro比较了解,基础要求略高。

vue-element-admin

本系列是站在后端Java开发的角度编写,所以对于vue不会详细介绍,着重于框架的使用。
这边采用vue-element-admin框架,该框架是基于vue+elementUI搭建admin管理界面,具体可以点击文中链接。对于非专业前端来说,只要会用这个框架基本就能完成独立的开发,这边也要感谢这位花裤衩大牛能够开源这么好的前端框架。阅读这边文章之前需要先查看花裤衩的文档,学会vue-element-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 <keep-alive> (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<SysRole> roleList) {
        //建立左侧菜单树
        List<SysMenuVO> menuVOTree = new ArrayList<>();
        //获取到所有菜单entity的set
        LinkedHashSet<SysMenu> menuSet = new LinkedHashSet<>();
        for (SysRole role : roleList) {
            menuSet.addAll(iSysMenuService.getMenu(role.getId()));
        }
        //获取到所有菜单VO的list
        List<SysMenuVO> 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<SysMenuVO> 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.学会采用递归算法来造树。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值