無限級選單/許可權樹該如何設計
在開發中我們經常會遇到:導航選單、部門選單、許可權樹、評論等功能。
這些功能都有共同的特點:
- 有父子關係
- 可無限遞迴
我們以導航選單為例, 我們將導航選單設定為動態的, 即從動態載入選單資料。
資料庫設計
適用於資料庫儲存的設計如下:
create table `menus` ( `id` int primary key auto_increment, `name` varchar(20) comment '選單名稱', `pid` int default 0 comment '父級 ID, 最頂級為 0', `order` int comment '排序, 序號越大, 越靠前' )
前端渲染
對於前端來說, 我們一般需要這種效果:
選單配置頁面:
對應的導航選單:
常用的樹形顯示外掛有: ofollow,noindex">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