前言
在開發中我們經常會遇到:導航菜單、部門菜單、權限樹、評論等功能。
這些功能都有共同的特點:
- 有父子關係
- 可無限遞歸
我們以導航菜單爲例, 我們將導航菜單設置爲動態的, 即從動態加載菜單數據。
數據庫設計
適用於數據庫存儲的設計如下:
create table `menus` ( `id` int primary key auto_increment, `name` varchar(20) comment '菜單名稱', `pid` int default 0 comment '父級 ID, 最頂級爲 0', `order` int comment '排序, 序號越大, 越靠前' )
前端渲染
對於前端來說, 我們一般需要這種效果:
菜單配置頁面:
對應的導航菜單:
常用的樹形顯示插件有: JsTree, zTree, Layui Tree, Bootstrap Tree View 等。
這些插件一般需要這兩種格式:
基礎格式:
[ { "id": 1, "name": "權限管理", "pid": 0, "order": 1 }, { "id": 2, "name": "用戶管理", "pid": 1, "order": 2 }, { "id": 3, "name": "角色管理", "pid": 1, "order": 3 }, { "id": 4, "name": "權限管理", "pid": 1, "order": 4 } ]
樹形格式:
[ { "id": 1, "name": "權限管理", "pid": 0, "order": 1, "children": [ { "id": 2, "name": "用戶管理", "pid": 1, "order": 2, "children": [] }, { "id": 3, "name": "角色管理", "pid": 1, "order": 3, "children": [] }, { "id": 4, "name": "權限管理", "pid": 1, "order": 4, "children": [] } ] } ]
有的插件這兩種格式都支持, 而有些只支持樹形結構, 但我們數據庫查詢出來的結果往往又是普通結構, 這時候我們就需要將普通格式轉換成樹形格式。
這個轉換一般是在服務端進行(因爲前端插件大多都是請求後臺的一個 URL 來接收 JSON 數據, 沒有提供加載數據後 - 渲染前的事件, 所以無法在前端完成轉換.)
數據轉換
首先有 Java 實體類:
public class Menu { private int id, private String name, private int pid // getter setter 略 }
數據庫查詢後的一般是在 List 中:
List<Menu> menus = xxxMapper.selectXXX();
然後我們需要將這個 List
轉換爲樹形結構, 首先定義一個樹形結構的 VO 類:
public class MenuTreeVO { private int id, private String name, private int pid, private List<MenuVo> children, // getter setter 略 }
轉換工具類:
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; /** * 轉換爲樹形 * @param list 所有節點 * @return 轉換後的樹結構菜單 */ public static List<MenuTreeVO> toTree(List<MenuTreeVO> list) { // 最初, 所有的 "菜單" 都是待用的 all = new ArrayList<>(list); // 拿到所有的頂級 "菜單" List<MenuTreeVO> roots = new ArrayList<>(); for (MenuTreeVO menuTreeVO : list) { if (menuTreeVO.getParentId() == 0) { roots.add(menuTreeVO); } } // 將所有頂級菜單從 "待用菜單列表" 中刪除 all.removeAll(roots); for (MenuTreeVO menuTreeVO : roots) { menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO));; } return roots; } /** * 遞歸函數 * 遞歸目的: 拿到子節點 * 遞歸終止條件: 沒有子節點 * @param parent 父節點 * @return 子節點 */ private static List<MenuTreeVO> getCurrentNodeChildren(MenuTreeVO parent) { // 判斷當前節點有沒有子節點, 沒有則創建一個空長度的 List, 有就使用之前已有的所有子節點. List<MenuTreeVO> childList = parent.getChildren() == null ? new ArrayList<>() : parent.getChildren(); // 從 "待用菜單列表" 中找到當前節點的所有子節點 for (MenuTreeVO child : all) { if (parent.getMenuId().equals(child.getParentId())) { childList.add(child); } } // 將當前節點的所有子節點從 "待用菜單列表" 中刪除 all.removeAll(childList); // 所有的子節點再尋找它們自己的子節點 for (MenuTreeVO menuTreeVO : childList) { menuTreeVO.setChildren(getCurrentNodeChildren(menuTreeVO)); } return childList; } }
調用方式:
// 從數據庫獲取 List<Menu> menus = xxxMapper.selectXXX(); // Menu 轉爲 MenuTreeVO List<MenuTreeVO> menuTreeVOS = new ArrayList<>(); for (Menu menu : menus) { MenuTreeVO menuTreeVO = new MenuTreeVO(); BeanUtils.copyProperties(menu, menuTreeVO); menuTreeVOS.add(menuTreeVO); } // 調用轉換方法 xxxUtil.toTree(menuTreeVOS); // 通過 Json 或 ModelAndView 返回給前臺.
附:模板引擎渲染
有時我們會使用模板引擎來渲染菜單, 但由於菜單是樹形結構的, 所以在模板引擎中單純的使用 for 是無法完成無限極菜單的渲染的.
這裏有一個很新奇的方法, 我以 thymeleaf
引擎爲例:
index.html 的導航部分:
<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}">系統管理</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 引用模板, 各種模板引擎都有這種功能, 然後判斷當前節點有沒有子節點, 有的話, 模板文件引用自身, 來完成遞歸.
結語
上述代碼是在開發一個 Shiro 的權限管理後臺的時候的一些思路和代碼, 完整的代碼可以參考: https://github.com/zhaojun1998/Shiro-Action