0、學完本文你或許可以收獲
-
感受一個樹工具從初始逐步優化完善的過程
-
樹工具封裝的設計思考與實現思路
-
最后收獲一款拿來即用的樹工具源代碼
對於前端樹組件有一定了解和使用過的同學可直接跳躍到第3章節開始。
1、樹長什么樣 ?
前端的樹組件大多數情況下出現在后端的管理系統中,比如我們常見的菜單樹、機構樹、某某分類樹、樹表格等。大致像下方圖片所展示的這樣。
菜單樹

機構樹

樹表格

大致上來說,前端樹的展現形式就是上面3張圖所列的幾種形式。而這種前端樹組件的展現構成需要依賴於后端返回的數據格式。
2、數據格式
結合我自身使用過的前端樹組件來說,大致可以分為如下兩種。
列表形式
[
{ id:1, pId:0, name:"父節點1"}
{ id:11, pId:1, name:"父節點11"},
{ id:111, pId:11, name:"葉子節點111"},
{ id:112, pId:11, name:"葉子節點112"},
{ id:113, pId:11, name:"葉子節點113"},
{ id:114, pId:11, name:"葉子節點114"},
{ id:12, pId:1, name:"父節點12"},
{ id:121, pId:12, name:"葉子節點121"},
{ id:122, pId:12, name:"葉子節點122"},
{ id:123, pId:12, name:"葉子節點123"},
{ id:124, pId:12, name:"葉子節點124"}
]
樹形結構
[{ name:"父節點1",
children: [
{
name:"父節點11",
children: [
{ name:"葉子節點111"},
{ name:"葉子節點112"},
{ name:"葉子節點113"},
{ name:"葉子節點114"}
]
},
{
name:"父節點12",
children: [
{ name:"葉子節點121"},
{ name:"葉子節點122"},
{ name:"葉子節點123"},
{ name:"葉子節點124"}
]
}
]
}]
本文所講的樹工具封裝主要是針對第二種數據格式樹形結構來說,因為第一種本身不需要特殊處理,也就不存在什么封裝,就是簡單的列表查詢展示,與一般數據列表數據格式的區別是多了數據ID與父ID屬性提供給前端進行樹組件的構造。
而第二種是要在列表形式的數據格式上進行轉換,形成如上所示的樹形結構。但是,我們發現里面沒有數據ID與父ID屬性,why ? 因為后端完成了數據層面樹結構的構造工作,前端樹組件再無需根據這兩個屬性進行樹結構的判斷構建,直接展示就OK,當然也不絕對,最終還得看前端的樹組件是否需要。
但一般都會保留這兩個屬性,因為除過樹組件自身的構造需求,業務處理上往往需要這兩個屬性,而后端樹工具要構造樹結構,那一定是需要數據ID與父ID的。
如果感覺上面說的麻煩你就記住一點,不管是列表結構還是樹形結構,始終保留數據ID與父ID兩個屬性就對了。
到這里又有一個新問題了,上面說了列表形式無需封裝什么可以直接使用,既然如此那用列表形式的結構就完了唄,為什么寫個工具類搞個樹結構出來呢 ?
原因是,前端樹組件的實現方式非常多,不同樹插件或組件需要的數據格式可能不一樣,有的列表、樹形格式都支持,有的僅支持列表或樹形的一種,所以為了滿足不同前端樹的展示需求,提供樹形結構的構造工具是必要的。
3、話不多說,先實現個初版
從上面的內容我們了解了前端樹組件的渲染展現需要后端提供滿足需求的數據格式,那么實際上也就決定了樹工具類的核心職責就是將一般的數據列表結構轉換為樹形結構,從而提供給前端使用。
解讀上面所述的核心職責,首先一般列表是什么列表,此處我們假設為菜單列表,這就有了第一個類MenuEntity,緊接着是轉換,誰轉換成誰 ?數據列表轉換樹結構,樹結構本身那應該就是個類,我們暫且叫它 TreeNode,結合我們第一步假設的菜單列表,那實際上就是 List< MenuEntity > 轉換為 List < TreeNode > ,如此就得到了第二個類TreeNode,最后還剩轉換這個動作誰去做 ? 那就是我們今天的主角 TreeUtil 了。
好,至此,通過分析樹工具類的核心職責,我們分析得到了三個類。
-
MenuEntity
-
TreeNode
-
TreeUtil
OK,有了上面的內容那就來個簡單的實現。
樹節點類
public class TreeNode {
// 樹節點ID
private String id;
// 樹節點名稱
private String name;
// 樹節點編碼
private String code;
// 樹節點鏈接
private String linkUrl;
// 樹節點圖標
private String icon;
// 父節點ID
private String parentId;
}
菜單類
public class MenuEntity {
// 菜單ID
private String id;
// 上級菜單ID
private String pid;
// 菜單名稱
private String name;
// 菜單編碼
private String code;
// 菜單圖標
private String icon;
// 菜單鏈接
private String url;
}
樹工具類
public class TreeUtil {
/**
* 樹構建
*/
public static List<TreeNode> build(List<TreeNode> treeNodes,Object parentId){
List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
for(TreeNode treeNode : treeNodes){
if(parentId.equals(treeNode.getParentId())){
finalTreeNodes.add(treeNode);
innerBuild(treeNodes,treeNode);
}
}
return finalTreeNodes;
}
private static void innerBuild(List<TreeNode> treeNodes,TreeNode parentNode){
for(TreeNode childNode : treeNodes){
if(parentNode.getId().equals(childNode.getParentId())){
List<TreeNode> children = parentNode.getChildren();
if(children == null){
children = CollectionUtil.newArrayList();
parentNode.setChildren(children);
}
children.add(childNode);
childNode.setParentId(parentNode.getId());
innerBuild(treeNodes,childNode);
}
}
}
}
樹工具類實現的兩個關鍵點,第一,樹構建的開始位置也就是從哪里開始構建,所以需要一個父ID參數來指定構建的起始位置,第二,構建到什么時候結束,不做限制的的話,我們的樹是可以無限延伸的,所以此處innerBuild方法進行遞歸操作。
測試代碼
public static void main(String[] args) {
// 1、模擬菜單數據
List<MenuEntity> menuEntityList = CollectionUtil.newArrayList();
menuEntityList.add(new MenuEntity("1","0","系統管理","sys","/sys"));
menuEntityList.add(new MenuEntity("11","1","用戶管理","user","/sys/user"));
menuEntityList.add(new MenuEntity("111","11","用戶添加","userAdd","/sys/user/add"));
menuEntityList.add(new MenuEntity("2","0","店鋪管理","store","/store"));
menuEntityList.add(new MenuEntity("21","2","商品管理","shop","/shop"));
// 2、MenuEntity -> TreeNode
List<TreeNode> treeNodes = CollectionUtil.newArrayList();
for(MenuEntity menuEntity : menuEntityList){
TreeNode treeNode = new TreeNode();
treeNode.setId(menuEntity.getId());
treeNode.setParentId(menuEntity.getPid());
treeNode.setCode(menuEntity.getCode());
treeNode.setName(menuEntity.getName());
treeNode.setLinkUrl(menuEntity.getUrl());
treeNodes.add(treeNode);
}
// 3、樹結構構建
List<TreeNode> treeStructureNodes = TreeUtil.build(treeNodes,"0");
Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}
收工,第一版簡單的樹工具就實現了。
4、迭代優化
1.0 這不是我的事
但是,通過測試代碼我們發現這個用起來不是太爽,要將菜單數據轉換為樹結構竟然需要我先把菜單列表轉換成樹結構的列表才能調用樹工具類的build方法,這里的轉換操作僅僅是屬性的拷貝,並未完成樹狀結構的生成構建,但這是調用者需要關心的嗎 ?很顯然TreeNode集合創建生成這個過程應該是樹工具類應該做的事情。所以做了如下調整。
1 調整了build方法參數,將原有treeNodes 調整為 menuEntityList,意味着將上面說的treeNodes構建構成交給TreeUtil去做。
2 新增了Convert類,並包含convert方法,該方法的職責是完成菜單實體到樹節點屬性的拷貝。
3 再次調整build方法參數,新增Convert轉換。
調整完成的結果,看下代碼。
樹工具
public class TreeUtil_1_0 {
// 新增的屬性轉換方法
public interface Convert<MenuEntity,TreeNode>{
public void convert(MenuEntity menuEntity, TreeNode treeNode);
}
/**
* 樹構建
*/
public static List<TreeNode> build(List<MenuEntity> menuEntityList,Object parentId,Convert<MenuEntity,TreeNode> convert){
// 原來調用方做的事情
List<TreeNode> treeNodes = CollectionUtil.newArrayList();
for(MenuEntity menuEntity: menuEntityList){
TreeNode treeNode = new TreeNode();
convert.convert(menuEntity,treeNode);
treeNodes.add(treeNode);
}
List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
for(TreeNode treeNode : treeNodes){
if(parentId.equals(treeNode.getParentId())){
finalTreeNodes.add(treeNode);
innerBuild(treeNodes,treeNode);
}
}
return finalTreeNodes;
}
private static void innerBuild(List<TreeNode> treeNodes,TreeNode parentNode){
for(TreeNode childNode : treeNodes){
if(parentNode.getId().equals(childNode.getParentId())){
List<TreeNode> children = parentNode.getChildren();
if(children == null){
children = CollectionUtil.newArrayList();
parentNode.setChildren(children);
}
children.add(childNode);
childNode.setParentId(parentNode.getId());
innerBuild(treeNodes,childNode);
}
}
}
}
測試代碼
public static void main(String[] args) {
// 1、模擬菜單數據
List<MenuEntity> menuEntityList = CollectionUtil.newArrayList();
menuEntityList.add(new MenuEntity("1","0","系統管理","sys","/sys"));
menuEntityList.add(new MenuEntity("11","1","用戶管理","user","/sys/user"));
menuEntityList.add(new MenuEntity("111","11","用戶添加","userAdd","/sys/user/add"));
menuEntityList.add(new MenuEntity("2","0","店鋪管理","store","/store"));
menuEntityList.add(new MenuEntity("21","2","商品管理","shop","/shop"));
// 2、樹結構構建
List<TreeNode> treeStructureNodes = TreeUtil_1_0.build(menuEntityList, "0", new Convert<MenuEntity, TreeNode>() {
@Override
public void convert(MenuEntity menuEntity, TreeNode treeNode) {
treeNode.setId(menuEntity.getId());
treeNode.setParentId(menuEntity.getPid());
treeNode.setCode(menuEntity.getCode());
treeNode.setName(menuEntity.getName());
treeNode.setLinkUrl(menuEntity.getUrl());
}
});
Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}
比較1.0與初版的測試代碼,發現少了樹節點列表構建的過程,屬性拷貝的工作作為回調過程在轉換過程中進行處理。
2.0 僅支持造菜單樹哪夠
1.0優化完后,我們來了新的需求,有個機構樹也需要生成,此時的樹工具僅支持了菜單樹,所以我們進行改造,讓其支持其他任何對象的樹生成。
改造點主要是將的TreeUtil中的菜單實體轉換為泛型,限於篇幅,就貼個核心方法的代碼
public static <T> List<TreeNode> build(List<T> list,Object parentId,Convert<T,TreeNode> convert){
List<TreeNode> treeNodes = CollectionUtil.newArrayList();
for(T obj : list){
TreeNode treeNode = new TreeNode();
convert.convert(obj,treeNode);
treeNodes.add(treeNode);
}
List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
for(TreeNode treeNode : treeNodes){
if(parentId.equals(treeNode.getParentId())){
finalTreeNodes.add(treeNode);
innerBuild(treeNodes,treeNode);
}
}
return finalTreeNodes;
}
如此一來,我們就可以支持任意類型的樹構造。
3.0 哥們,你返回的屬性不夠用啊
前兩點比較容易想到,也比較容易實現,但這時候前端同學拋來了新的問題,哥們,你返回的樹節點屬性不夠用啊,你看我這界面。需要備注你沒返回來啊。

好吧,這種情況確實沒考慮到。
要滿足上述需求,簡單做法就將remark屬性直接添加到 TreeNode 類中,Convert中賦下值,這不就滿足了,但想想又不對,今天這個前端伙計缺個remark,明天可能別的伙計又缺個其他屬性,全加到TreeNode中,TreeNode到底是樹節點還是業務實體,所以不能這么搞。
這里要處理成可擴展,同時滿足開閉原則,所以此處比較妥的處理方式是繼承,TreeNode屬性滿足不了的情況下,通過繼承擴展具體業務的樹節點來實現。
具體改造點如下
1 新增菜單實體擴展樹節點如下
public class MenuEntityTreeNode extends TreeNode {
// 擴展備注屬性
private String remark;
// 省略set get ...
}
2 改造TreeUtil.build方法參數,新增TreeNode Class類型參數,如下
/**
* 樹構建
*/
public static <T,E extends TreeNode> List<E> build(List<T> list,Object parentId,Class<E> treeNodeClass,Convert<T,E> convert){
List<E> treeNodes = CollectionUtil.newArrayList();
for(T obj : list){
E treeNode = (E)ReflectUtil.newInstance(treeNodeClass);
convert.convert(obj, treeNode);
treeNodes.add(treeNode);
}
List<E> finalTreeNodes = CollectionUtil.newArrayList();
for(E treeNode : treeNodes){
if(parentId.equals(treeNode.getParentId())){
finalTreeNodes.add((E)treeNode);
innerBuild(treeNodes,treeNode);
}
}
return finalTreeNodes;
}
測試代碼
public static void main(String[] args) {
// ...此處省略模擬數據創建過程
// 2、樹結構構建
List<MenuEntityTreeNode> treeStructureNodes = TreeUtil_3_0.build(menuEntityList, "0",MenuEntityTreeNode.class,new TreeUtil_3_0.Convert<MenuEntity,MenuEntityTreeNode>(){
@Override
public void convert(MenuEntity object, MenuEntityTreeNode treeNode) {
treeNode.setId(object.getId());
treeNode.setParentId(object.getPid());
treeNode.setCode(object.getCode());
treeNode.setName(object.getName());
treeNode.setLinkUrl(object.getUrl());
// 新增的業務屬性
treeNode.setRemark("添加備注屬性");
}
});
Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}
如此一來,不同業務場景下需要添加不同的屬性時,即可做到可擴展,且對現有代碼不造成任何影響和改動。
4.0 哥們,我的屬性名不叫code
完成了3.0版本,基本上大部分需求就都可以滿足了,但是這時候前端同學又拋來了新的問題,哥們,你返回的樹節點編號屬性是code,但我這邊的叫number,對應不上,我這邊調整的話影響比較大,你看后端返回的時候能不能處理下。
code屬性名肯定是不能調整的,因為其他模塊樹的節點編號都叫code。
那怎么辦 ?其實也簡單,跟3.0版本一樣,在擴展的業務樹節點去加個屬性,這樣問題是解決了,但萬一出現所有treeNode的屬性名都跟前端需要的不對應這種極端情況,那意味着所有樹屬性都需要自行擴展定義,這種豈不是返回了沒什么用的父TreeNode心中的所有屬性。序列化時倒是可以控制,為空的不進行序列化,但不是依賴序列化框架了么。還有沒有其他辦法。
稍微整理下需求,就是樹節點屬性在返回前端時要能夠支持自定義屬性名。
類屬性定義好就改不了了,怎么自定義,除了新增類和改現有的屬性,還有什么辦法呢 ?這時候我們應該想到map
具體怎么做
1 首先,定義新的類TreeNodeMap,看名字就知道基於map實現
public class TreeNodeMap extends HashMap {
private TreeNodeConfig treeNodeConfig;
public TreeNodeMap(){
this.treeNodeConfig = TreeNodeConfig.getDefaultConfig();
}
public TreeNodeMap(TreeNodeConfig treeNodeConfig){
this.treeNodeConfig = treeNodeConfig;
}
public <T> T getId() {
return (T)super.get(treeNodeConfig.getIdKey());
}
public void setId(String id) {
super.put(treeNodeConfig.getIdKey(), id);
}
public <T> T getParentId() {
return (T)super.get(treeNodeConfig.getParentIdKey());
}
public void setParentId(String parentId) {
super.put(treeNodeConfig.getParentIdKey(), parentId);
}
public <T> T getName() {
return (T)super.get(treeNodeConfig.getNameKey());
}
public void setName(String name) {
super.put(treeNodeConfig.getNameKey(), name);
}
public <T> T getCode() {
return (T)super.get(treeNodeConfig.getCodeKey());
}
public TreeNodeMap setCode(String code) {
super.put(treeNodeConfig.getCodeKey(), code);
return this;
}
public List<TreeNodeMap> getChildren() {
return (List<TreeNodeMap>)super.get(treeNodeConfig.getChildrenKey());
}
public void setChildren(List<TreeNodeMap> children) {
super.put(treeNodeConfig.getChildrenKey(),children);
}
public void extra(String key,Object value){
super.put(key,value);
}
}
2 既然支持屬性名自定義,新增配置類TreeNodeConfig來完成這個事情,同時提供默認屬性名
public class TreeNodeConfig {
// 默認屬性的單例對象
private static TreeNodeConfig defaultConfig = new TreeNodeConfig();
// 樹節點默認屬性常量
static final String TREE_ID = "id";
static final String TREE_NAME = "name";
static final String TREE_CODE = "code";
static final String TREE_CHILDREN = "children";
static final String TREE_PARENT_ID = "parentId";
// 屬性
private String idKey;
private String codeKey;
private String nameKey;
private String childrenKey;
private String parentIdKey;
public String getIdKey() {
return getOrDefault(idKey,TREE_ID);
}
public void setIdKey(String idKey) {
this.idKey = idKey;
}
public String getCodeKey() {
return getOrDefault(codeKey,TREE_CODE);
}
public void setCodeKey(String codeKey) {
this.codeKey = codeKey;
}
public String getNameKey() {
return getOrDefault(nameKey,TREE_NAME);
}
public void setNameKey(String nameKey) {
this.nameKey = nameKey;
}
public String getChildrenKey() {
return getOrDefault(childrenKey,TREE_CHILDREN);
}
public void setChildrenKey(String childrenKey) {
this.childrenKey = childrenKey;
}
public String getParentIdKey() {
return getOrDefault(parentIdKey,TREE_PARENT_ID);
}
public void setParentIdKey(String parentIdKey) {
this.parentIdKey = parentIdKey;
}
public String getOrDefault(String key,String defaultKey){
if(key == null) {
return defaultKey;
}
return key;
}
public static TreeNodeConfig getDefaultConfig(){
return defaultConfig;
}
}
3 最后,改造TreeUtil.build 方法,基於2.0版本,只需將TreeNode替換成TreeNodeMap即可。
/**
* 樹構建
*/
public static <T> List<TreeNodeMap> build(List<T> list,Object parentId,Convert<T,TreeNodeMap> convert){
List<TreeNodeMap> treeNodes = CollectionUtil.newArrayList();
for(T obj : list){
TreeNodeMap treeNode = new TreeNodeMap();
convert.convert(obj,treeNode);
treeNodes.add(treeNode);
}
List<TreeNodeMap> finalTreeNodes = CollectionUtil.newArrayList();
for(TreeNodeMap treeNode : treeNodes){
if(parentId.equals(treeNode.getParentId())){
finalTreeNodes.add(treeNode);
innerBuild(treeNodes,treeNode);
}
}
return finalTreeNodes;
}
測試代碼
public static void main(String[] args) {
// ... 省略菜單模擬數據的創建過程
TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
// 自定義屬性名
treeNodeConfig.setCodeKey("number");
List<TreeNodeMap> treeNodes = TreeUtil_4_0.build(menuEntityList, "0",treeNodeConfig,new TreeUtil_4_0.Convert<MenuEntity, TreeNodeMap>() {
@Override
public void convert(MenuEntity object, TreeNodeMap treeNode) {
treeNode.setId(object.getId());
treeNode.setParentId(object.getPid());
treeNode.setCode(object.getCode());
treeNode.setName(object.getName());
// 屬性擴展
treeNode.extra("extra1","123");
}
});
Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeNodes)));
}
經過上面的改造,我們實現了樹節點屬性的自定義,順便還實現了屬性可擴展,一舉兩得。
3、總結
目前這個程度可能仍有些場景無法滿足,但是對於大部分的問題場景基於3.0或4.0版本稍加改造應該都可以解決。剩下的就結合場景再酌情優化調整。
4、源代碼&視頻
5、更多精彩
覺得還行,動動手指留個贊。
以上就是今天的內容,我們下期見。