手把手教你如何玩转菜单生成功能(WEB)

情景引入

  • 小白:起床起床,,,我又遇到一个麻烦事了。
  • 我:怎么了呢?算好今天我醒来得早,就不责备你了。
  • 小白:就是,昨晚我躺在床上进行思考问题的时候,突然想到我们用的很多系统里面都有菜单,但是,我注意到不同的用户登录之后显示的都不一样耶。
  • 我:当然了,不同的用户看到的未必是一样,只是为了进行页面功能的控制呀。这有什么奇怪的呢?
  • 小白:emmm,我之前都是做的很多业务功能,都没有关注这方面的问题,所以,我想请教下这是如何做到的呢?因为这个很普遍,我想多学学这里面的知识。
  • 我:今天太阳是从西边出来了吗?你都会这么思考了,看在你这么爱学习的份上,我就给你好好说说这方面的知识把。
  • 小白:好呀好呀~又可以学习新东西咯。。。。

简介

首先,先用图来引入正题。
GUNS开源系统的用户菜单
我们观察上面的图,我们可以发现存在着很多不同的菜单列表,而且它其实是一种动态的显示功能。很明显,这是为了适应不同的用户而显示不同的菜单,以免发生系统功能的混乱(比如让一个用户拥有删库的功能,这不是很恐怖的吗?)来实现系统的稳定性和实用性。
菜单管理其实对于一个后台系统是非常非常普遍的,麻雀虽小五脏俱全,它所包含的知识其实是很多的。总的来说,菜单管理是一个系统的“门面”,所以,它关系到这个系统的使用。那么如何进行这个功能的开发呢?请认真听我一步步来为大家进行分析。

开发步骤

这里的话,我就不单独进行编写了,我就通过GUNS开源框架来帮助大家一步步剖析这个功能的实现(大家应该多多看看开源框架的实现方式,这个也是提升我们编码能力的一个方法,可能过程很枯燥,但是一步步下来,收获也是非常多的)。
环境:IDEA+Maven + SpringBoot + Mybatis + Mysql + windows7

  1. 数据库设计
    备注:既然我前面已经提到,菜单功能是一个动态特性,那么为什么拥有动态,其实就是在数据库上进行的“手脚”。我们都知道权限同样是一种动态特性,这样才能够保证不同的用户能够进行系统的不同处理操作,否则都一样,那么系统就无法正常的进行管理信息。同理,菜单功能的实现也正是基于权限系统的开发设计,所以,我们可以设计如下的数据库结构。
    表一:用户表
    在这里插入图片描述
    表二:角色表
    在这里插入图片描述
    表三:系统资源表(菜单功能表)
    在这里插入图片描述
    表四:用户-角色-资源表
    在这里插入图片描述
    备注:大体设计的话就是上面的这样子,如果还有什么特别的功能的话,那么就对应的修改字段就好了,关键在于让“用户-角色-系统资源”能够有关联关系。
  2. 获取当前登录用户的显示菜单信息
    温馨提示:其实这个就是在当用户进行登录验证之后跳转到主页的时候进行的操作。
    备注:这是菜单构建的关键地方,可以分为几步进行处理:
    (1)获取当前用户的角色信息(主要就是:roleId)
    这个很简单,就是简单的查询"用户表"即可
    controller层:
@Autowired
 private UserService userService;   //对用户信息操作的service层
/**
     * 跳转到主页
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(Model model) {
        //获取角色列表(根据用户名获取(当然,这个根据你的需要,也可以通过其他的用户表字段进行获取都可以的))
        List<Integer> roleList = userService.getRoleList(account);
        if (roleList == null || roleList.size() == 0) {
            ShiroKit.getSubject().logout();
            model.addAttribute("tips", "该用户没有角色,无法登陆");
            return "/login.html";
        }
    }

mapper层:

select roleid
from user
where account = "XXXXX"

备注:其实对于用户的相关信息,我们一般都在登录验证的时候就会进行保存到session或者redis中,所以,我们可以直接从缓存这里面获取我们想要的信息的。
(2)通过菜单资源信息构建返回给前端的Json格式数据,便于进行菜单显示。
我们知道,前端都是根据我们后台给的一些有规则的结构数据进行解析,从而达到一种数据显示的效果,所以,既然如此,那么对于菜单的数据格式,肯定也不能是随便的,我们可以构建如下的菜单bean对象:

package cn.stylefeng.guns.core.common.node;
import cn.stylefeng.roses.kernel.model.enums.YesOrNotEnum;
import java.util.*;
public class MenuNode implements Comparable {

    /**
     * 节点id
     */
    private Long id;

    /**
     * 父节点
     */
    private Long parentId;

    /**
     * 节点名称
     */
    private String name;

    /**
     * 按钮级别
     */
    private Integer levels;

    /**
     * 按钮级别
     */
    private Integer ismenu;

    /**
     * 按钮的排序
     */
    private Integer num;

    /**
     * 节点的url
     */
    private String url;

    /**
     * 节点图标
     */
    private String icon;

    /**
     * 子节点的集合
     */
    private List<MenuNode> children;

    /**
     * 查询子节点时候的临时集合
     */
    private List<MenuNode> linkedList = new ArrayList<MenuNode>();

    public MenuNode() {
        super();
    }

    public MenuNode(Long id, Long parentId) {
        super();
        this.id = id;
        this.parentId = parentId;
    }

    public Integer getLevels() {
        return levels;
    }

    public void setLevels(Integer levels) {
        this.levels = levels;
    }

    public String getIcon() {
        return icon;
    }

    public void setIcon(String icon) {
        this.icon = icon;
    }

    public static MenuNode createRoot() {
        return new MenuNode(0L, -1L);
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public List<MenuNode> getChildren() {
        return children;
    }

    public void setChildren(List<MenuNode> children) {
        this.children = children;
    }

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }

    public Integer getIsmenu() {
        return ismenu;
    }

    public void setIsmenu(Integer ismenu) {
        this.ismenu = ismenu;
    }

    @Override
    public String toString() {
        return "MenuNode{" +
                "id=" + id +
                ", parentId=" + parentId +
                ", name='" + name + '\'' +
                ", levels=" + levels +
                ", num=" + num +
                ", url='" + url + '\'' +
                ", icon='" + icon + '\'' +
                ", children=" + children +
                ", linkedList=" + linkedList +
                '}';
    }

    /**
     * 重写排序比较接口,首先根据等级排序,然后更具排序字段排序
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Object o) {
        MenuNode menuNode = (MenuNode) o;
        Integer num = menuNode.getNum();
        Integer levels = menuNode.getLevels();
        if (num == null) {
            num = 0;
        }
        if (levels == null) {
            levels = 0;
        }
        if (this.levels.compareTo(levels) == 0) {
            return this.num.compareTo(num);
        } else {
            return this.levels.compareTo(levels);
        }

    }

    /**
     * 构建页面菜单列表
     */
    public static List<MenuNode> buildTitle(List<MenuNode> nodes) {
        if (nodes.size() <= 0) {
            return nodes;
        }
        //剔除非菜单
        nodes.removeIf(node -> !node.getIsmenu().equals(YesOrNotEnum.Y.getCode()));
        //对菜单排序,返回列表按菜单等级,序号的排序方式排列
        Collections.sort(nodes);
        return mergeList(nodes, nodes.get(nodes.size() - 1).getLevels(), null);
    }

    /**
     * 递归合并数组为子数组,最后返回第一层
     *
     * @param menuList
     * @param listMap
     * @return
     */
    private static List<MenuNode> mergeList(List<MenuNode> menuList, int rank, Map<Long, List<MenuNode>> listMap) {
        //保存当次调用总共合并了多少元素
        int n;
        //保存当次调用总共合并出来的list
        Map<Long, List<MenuNode>> currentMap = new HashMap<>();
        //由于按等级从小到大排序,需要从后往前排序
        //判断该节点是否属于当前循环的等级,不等于则跳出循环
        for (n = menuList.size() - 1; n >= 0 && menuList.get(n).getLevels() == rank; n--) {
            //判断之前的调用是否有返回以该节点的id为key的map,有则设置为children列表。
            if (listMap != null && listMap.get(menuList.get(n).getId()) != null) {
                menuList.get(n).setChildren(listMap.get(menuList.get(n).getId()));
            }
            if (menuList.get(n).getParentId() != null && menuList.get(n).getParentId() != 0) {
                //判断当前节点所属的pid是否已经创建了以该pid为key的键值对,没有则创建新的链表
                currentMap.computeIfAbsent(menuList.get(n).getParentId(), k -> new LinkedList<>());
                //将该节点插入到对应的list的头部
                currentMap.get(menuList.get(n).getParentId()).add(0, menuList.get(n));
            }
        }
        if (n < 0) {
            return menuList;
        } else {
            return mergeList(menuList.subList(0, n + 1), menuList.get(n).getLevels(), currentMap);
        }
    }
}

备注:建议大家可以好好的分析一下这个结构。
(3)根据roleId获取对应的资源信息(主要就是:用户-角色-资源表以及系统资源表的操作)
controller层:

@Autowired
 private UserService userService;   //对用户信息操作的service层
/**
     * 跳转到主页
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(Model model) {
        //获取角色列表
        List<Integer> roleList = userService.getRoleList();
        if (roleList == null || roleList.size() == 0) {
            ShiroKit.getSubject().logout();
            model.addAttribute("tips", "该用户没有角色,无法登陆");
            return "/login.html";
        }
        List<MenuNode> menus = menuService.getMenusByRoleIds(roleList);
        List<MenuNode> titles = MenuNode.buildTitle(menus);
        //将其便于在前端进行获取
        model.addAttribute("titles", titles);
        return "/index.html";
    }

mapper层:

<select id="getMenusByRoleIds" resultType="cn.stylefeng.guns.core.common.node.MenuNode">
        SELECT
        m1.id AS id,
        m1.icon AS icon,
        (
        CASE
        WHEN (m2.id = 0 OR m2.id IS NULL) THEN
        0
        ELSE
        m2.id
        END
        ) AS parentId,
        m1.NAME as name,
        m1.url as url,
        m1.levels as levels,
        m1.ismenu as ismenu,
        m1.num as num
        FROM
        sys_menu m1
        LEFT join sys_menu m2 ON m1.pcode = m2. CODE
        INNER JOIN (
        SELECT
        ID
        FROM
        sys_menu
        WHERE
        ID IN (
        SELECT
        menuid
        FROM
        sys_relation rela
        WHERE
        rela.roleid IN
        <foreach collection="list" index="index" item="i" open="(" separator="," close=")">
            #{i}
        </foreach>
        )
        ) m3 ON m1.id = m3.id
        where m1.ismenu = 1
        order by levels,num asc
    </select>

备注:对于mapper的这个sql语句,其实并不是唯一的,还有很多种的写法,但是你需要的是理解到底如何获取,并且获取哪些有用的字段,这个就是关键,所以,这是一种参考的写法而已,也可以有不同的写法,大家也可以进行测试看是不是得到结果正确就好了。

  1. 菜单返回的Json格式
    就把上面的menuNode菜单的格式给大家用Json格式进行显示,因为这样的话,我们前端才好进行迭代的解析,从而生成对应数目的菜单和形式。
[
	{
		"children":"",
		"icon":"fa-rocket",
		"id":"145",
		"ismenu":1,
		"levels":1,
		"name":"通知",
		"num":1,
		"parentId":"0",
		"url":"/notice/hello"
	},
	{
		"children":"",
		"icon":"fa-leaf",
		"id":"149",
		"ismenu":1,
		"levels":1,
		"name":"接口文档",
		"num":2,
		"parentId":"0",
		"url":"/swagger-ui.html"
	},
	{
		"children":"",
		"icon":"fa-code",
		"id":"148",
		"ismenu":1,
		"levels":1,
		"name":"代码生成",
		"num":3,
		"parentId":"0",
		"url":"/code"
	},
	{
		"children":[{
			"children":"",
			"icon":"",
			"id":"106",
			"ismenu":1,
			"levels":2,
			"name":"用户管理",
			"num":1,
			"parentId":"105",
			"url":"/mgr"
		},{
			"children":"",
			"icon":"",
			"id":"114",
			"ismenu":1,
			"levels":2,
			"name":"角色管理",
			"num":2,
			"parentId":"105",
			"url":"/role"
		},{
			"children":"",
			"icon":"",
			"id":"131",
			"ismenu":1,
			"levels":2,
			"name":"部门管理",
			"num":3,
			"parentId":"105",
			"url":"/dept"
		}],
		"icon":"fa-user",
		"id":"105",
		"ismenu":1,
		"levels":1,
		"name":"系统管理",
		"num":4,
		"parentId":"0",
		"url":"#"
	}
]
  1. 前端解析
    既然我们已经知道了后台返回的格式内容,所以,前端根据需要进行对应需求的循环解析就好了,所以,这个就不多说了。

Json格式

这里的话,就多说一下关于Json格式。

解释

百度百科:JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。
通俗的讲解:其实就是一种类似于Key-value的格式,为什么这样说呢?因为对于Json格式来说,我们也是用的这样的形式。具体的话,大家可以看看上面的菜单返回的格式,一下就能够明白。

{
"people":[
{
"firstName": "Brett",
"lastName":"McLaughlin"       
},
{
"firstName":"Jason",
"lastName":"Hunter"
}
]
}

场景

(1)前端传递给后端数据(ajax)
(2)后端返回结构化比较强的数据,便于让前端进行解析

与XML的比较

(1)可读性
JSON和XML的可读性差不多,一边是简易的语法,一边是规范的标签形式。JSON相对轻便点。
(2)可扩展性
XML天生有很好的扩展性,JSON当然也有,不过JSON在Javascript的基础环境中,可以存储Javascript复合对象,有着xml不可比拟的优势。
(3)编码难度
XML有丰富的编码工具,比如Dom4j、Dom、SAX等,JSON也有提供的工具。无工具的情况下,相信熟练的开发人员一样能很快的写出想要的xml文档和JSON字符串,不过,xml文档要多很多结构上的字符。
(4)解码难度
XML的解析方式有两种:
一种是通过文档模型解析,也就是通过父标签索引出一组标记。
另外一种方法是遍历节点。
JSON也同样如此。如果预先知道JSON结构的情况下,使用JSON进行数据传递简直是太美妙了,可以写出很实用美观可读性强的代码。如果你是纯粹的前台开发人员,一定会非常喜欢JSON。但是如果你是一个应用开发人员,就不是那么喜欢了,毕竟xml才是真正的结构化标记语言,用于进行数据传递

后端解析Json的工具

(1)jackson
(2)fastJson(阿里)
(3)gson
(4)json-lib

总结

菜单管理是一个普遍的功能,但是它包含的知识点却是很多,对于web程序开发的流程都有所触及,所以,我们应该学会的是对这里面流程和思想进行学习。
额外,对于菜单管理,我们应该对权限管理也要能够掌握,它们两者其实是有很多的关联关系,因为它们操作的表都是一样的,只是说对于里面的流程处理不是一样的,所以,建议大家再去掌握下权限管理,而我权限管理的文章也写了比较多的了(可以去我主页查看哦!)。

建议

大家可以去看看开源后台框架的源码,这样能够帮助自己进行更好的理解,做到举一反三,提供问题的思考和处理能力。
推荐一个后台管理系统:GUNS开源系统
码云地址:https://gitee.com/naan1993/guns/

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值