前言
在開發(fā)中我們經(jīng)常會(huì)遇到:導(dǎo)航菜單绢掰、部門菜單痒蓬、權(quán)限樹、評(píng)論等功能滴劲。
這些功能都有共同的特點(diǎn):
- 有父子關(guān)系
- 可無(wú)限遞歸
我們以導(dǎo)航菜單為例, 我們將導(dǎo)航菜單設(shè)置為動(dòng)態(tài)的, 即從動(dòng)態(tài)加載菜單數(shù)據(jù)攻晒。
數(shù)據(jù)庫(kù)設(shè)計(jì)
適用于數(shù)據(jù)庫(kù)存儲(chǔ)的設(shè)計(jì)如下:
create table `menus`
(
`id` int primary key auto_increment,
`name` varchar(20) comment '菜單名稱',
`pid` int default 0 comment '父級(jí) ID, 最頂級(jí)為 0',
`order` int comment '排序, 序號(hào)越大, 越靠前'
)
前端渲染
對(duì)于前端來(lái)說(shuō), 我們一般需要這種效果:
菜單配置頁(yè)面:
對(duì)應(yīng)的導(dǎo)航菜單:
常用的樹形顯示插件有: JsTree, zTree, Layui Tree, Bootstrap Tree View 等。
這些插件一般需要這兩種格式:
基礎(chǔ)格式:
[
{
"id": 1,
"name": "權(quán)限管理",
"pid": 0,
"order": 1
},
{
"id": 2,
"name": "用戶管理",
"pid": 1,
"order": 2
},
{
"id": 3,
"name": "角色管理",
"pid": 1,
"order": 3
},
{
"id": 4,
"name": "權(quán)限管理",
"pid": 1,
"order": 4
}
]
樹形格式:
[
{
"id": 1,
"name": "權(quán)限管理",
"pid": 0,
"order": 1,
"children": [
{
"id": 2,
"name": "用戶管理",
"pid": 1,
"order": 2,
"children": []
},
{
"id": 3,
"name": "角色管理",
"pid": 1,
"order": 3,
"children": []
},
{
"id": 4,
"name": "權(quán)限管理",
"pid": 1,
"order": 4,
"children": []
}
]
}
]
有的插件這兩種格式都支持, 而有些只支持樹形結(jié)構(gòu), 但我們數(shù)據(jù)庫(kù)查詢出來(lái)的結(jié)果往往又是普通結(jié)構(gòu), 這時(shí)候我們就需要將普通格式轉(zhuǎn)換成樹形格式班挖。
這個(gè)轉(zhuǎn)換一般是在服務(wù)端進(jìn)行(因?yàn)榍岸瞬寮蠖喽际钦?qǐng)求后臺(tái)的一個(gè) URL 來(lái)接收 JSON 數(shù)據(jù), 沒(méi)有提供加載數(shù)據(jù)后 - 渲染前的事件, 所以無(wú)法在前端完成轉(zhuǎn)換.)
數(shù)據(jù)轉(zhuǎn)換
首先有 Java 實(shí)體類:
public class Menu {
private int id,
private String name,
private int pid
// getter setter 略
}
數(shù)據(jù)庫(kù)查詢后的一般是在 List 中:
List<Menu> menus = xxxMapper.selectXXX();
然后我們需要將這個(gè) List
轉(zhuǎn)換為樹形結(jié)構(gòu), 首先定義一個(gè)樹形結(jié)構(gòu)的 VO 類:
public class MenuTreeVO {
private int id,
private String name,
private int pid,
private List<MenuVo> children,
// getter setter 略
}
轉(zhuǎn)換工具類:
package im.zhaojun.util;
import im.zhaojun.model.vo.MenuTreeVO;
import java.util.ArrayList;
import java.util.List;
public class TreeUtil {
/**
* 所有待用"菜單"
*/
private static List<MenuTreeVO> all = null;
/**
* 轉(zhuǎn)換為樹形
* @param list 所有節(jié)點(diǎn)
* @return 轉(zhuǎn)換后的樹結(jié)構(gòu)菜單
*/
public static List<MenuTreeVO> toTree(List<MenuTreeVO> list) {
// 最初, 所有的 "菜單" 都是待用的
all = new ArrayList<>(list);
// 拿到所有的頂級(jí) "菜單"
List<MenuTreeVO> roots = new ArrayList<>();
for (MenuTreeVO menuTreeVO : list) {
if (menuTreeVO.getParentId() == 0) {
roots.add(menuTreeVO);
}
}
// 將所有頂級(jí)菜單從 "待用菜單列表" 中刪除
all.removeAll(roots);
for (MenuTreeVO menuTreeVO : roots) {
menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO));;
}
return roots;
}
/**
* 遞歸函數(shù)
* 遞歸目的: 拿到子節(jié)點(diǎn)
* 遞歸終止條件: 沒(méi)有子節(jié)點(diǎn)
* @param parent 父節(jié)點(diǎn)
* @return 子節(jié)點(diǎn)
*/
private static List<MenuTreeVO> getCurrentNodeChildren(MenuTreeVO parent) {
// 判斷當(dāng)前節(jié)點(diǎn)有沒(méi)有子節(jié)點(diǎn), 沒(méi)有則創(chuàng)建一個(gè)空長(zhǎng)度的 List, 有就使用之前已有的所有子節(jié)點(diǎn).
List<MenuTreeVO> childList = parent.getChildren() == null ? new ArrayList<>() : parent.getChildren();
// 從 "待用菜單列表" 中找到當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)
for (MenuTreeVO child : all) {
if (parent.getMenuId().equals(child.getParentId())) {
childList.add(child);
}
}
// 將當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)從 "待用菜單列表" 中刪除
all.removeAll(childList);
// 所有的子節(jié)點(diǎn)再尋找它們自己的子節(jié)點(diǎn)
for (MenuTreeVO menuTreeVO : childList) {
menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO));
}
return childList;
}
}
調(diào)用方式:
// 從數(shù)據(jù)庫(kù)獲取
List<Menu> menus = xxxMapper.selectXXX();
// Menu 轉(zhuǎn)為 MenuTreeVO
List<MenuTreeVO> menuTreeVOS = new ArrayList<>();
for (Menu menu : menus) {
MenuTreeVO menuTreeVO = new MenuTreeVO();
BeanUtils.copyProperties(menu, menuTreeVO);
menuTreeVOS.add(menuTreeVO);
}
// 調(diào)用轉(zhuǎn)換方法
xxxUtil.toTree(menuTreeVOS);
// 通過(guò) Json 或 ModelAndView 返回給前臺(tái).
附:模板引擎渲染
有時(shí)我們會(huì)使用模板引擎來(lái)渲染菜單, 但由于菜單是樹形結(jié)構(gòu)的, 所以在模板引擎中單純的使用 for 是無(wú)法完成無(wú)限極菜單的渲染的.
這里有一個(gè)很新奇的方法, 我以 thymeleaf
引擎為例:
index.html 的導(dǎo)航部分:
<div class="left-nav">
<div id="side-nav">
<ul id="nav">
<th:block th:include="public::menu(${menus})"/>
</ul>
</div>
</div>
public.html 公共模板部分:
<th:block th:fragment="menu(menus)">
<li th:each="menu:${menus}">
<a href="javascript:;">
<i class="iconfont"></i>
<cite th:text="${menu.menuName}">系統(tǒng)管理</cite>
<i class="iconfont nav_right"></i>
</a>
<ul class="sub-menu">
<li th:each="child:${menu.children}">
<a th:if="${#lists.isEmpty(child.children)}" data-th-_href="${child.url}" _href="users">
<i class="iconfont"></i>
<cite th:text="${child.menuName}">用戶管理</cite>
</a>
<th:block th:unless="${#lists.isEmpty(child.children)}" th:include="this::menu(${child})" />
</li>
</ul>
</li>
</th:block>
基本邏輯就是使用 include 引用模板, 各種模板引擎都有這種功能, 然后判斷當(dāng)前節(jié)點(diǎn)有沒(méi)有子節(jié)點(diǎn), 有的話, 模板文件引用自身, 來(lái)完成遞歸.
結(jié)語(yǔ)
上述代碼是在開發(fā)一個(gè) Shiro 的權(quán)限管理后臺(tái)的時(shí)候的一些思路和代碼, 完整的代碼可以參考: https://github.com/zhaojun1998/Shiro-Action