多叉树结合JavaScript树形组件实现无限级树形结构(一种构建多级有序树形结构JSON(或XML)数据源的方法)

一、问题研究的背景和意义

在Web应用程序开发领域,基于Ajax技术的JavaScript树形组件已经被广泛使用,它用来在Html页面上展现具有层次结构的数据项。目前市场上常见的JavaScript框架及组件库中均包含自己的树形组件,例如jQuery、Ext JS等,还有一些独立的树形组件,例如dhtmlxTree等,这些树形组件完美的解决了层次数据的展示问题。展示离不开数据,树形组件主要利用Ajax技术从服务器端获取数据源,数据源的格式主要包括JSON、XML等,而这些层次数据一般都存储在数据库中。“无限级树形结构”,顾名思义,没有级别的限制,它的数据通常来自数据库中的无限级层次数据,这种数据的存储表通常包括id和parentId这两个字段,以此来表示数据之间的层次关系。现在问题来了,既然树形组件的数据源采用JSON或XML等格式的字符串来组织层次数据,而层次数据又存储在数据库的表中,那么如何建立起树形组件与层次数据之间的关系,换句话说,如何将数据库中的层次数据转换成对应的层次结构的JSON或XML格式的字符串,返回给客户端的JavaScript树形组件?这就是我们要解决的关键技术问题。本文将以目前市场上比较知名的Ext JS框架为例,讲述实现无限级树形结构的方法,该方法同样适用于其它类似的JavaScript树形组件。

Ext JS框架是富客户端开发中出类拔萃的框架之一。在Ext的UI组件中,树形组件无疑是最为常用的组件之一,它用来实现树形结构的视图。TreeNode用来实现静态的树形结构,AsyncTreeNode用来实现动态的异步加载树形结构,后者最为常用,它通过接收服务器端返回来的JSON格式的数据,动态生成树形结构节点。动态生成树有两种思路:一种是一次性生成全部树节点,另一种是逐级加载树节点(利用Ajax,每次点击节点时查询下一级节点)。对于大数据量的树节点来说,逐级加载是比较合适的选择,但是对于小数据量的树节点来说,一次性生成全部节点应该是最为合理的方案。在实际应用开发中,一般不会遇到特别大数据量的场景,所以一次性生成全部树节点是我们重点研究的技术点,也就是本文要解决的关键技术问题。本文以基于Ext JS的应用系统为例,讲述如何将数据库中的无限级层次数据一次性在界面中生成全部树节点(例如在界面中以树形方式一次性展示出银行所有分支机构的信息),同时对每一个层次的节点按照某一属性和规则排序,展示出有序的树形结构。

解决一次性构造无限级树形结构的问题,可以拓展出更多的应用场景,例如树形结构表格TreeGrid,一次性生成树形表格,对树形表格进行完整分页,对表格列进行全排序;或者可以利用本文的思路扩展出其他的更复杂的应用场景。

先看两个图例,有个直观上的认识:

图一,银行分支机构树形结构


图二,树形结构表格


二、详细设计方案

让我们先看两段代码片段:

文件一,branchTree.html (Ext树形组件页面)

Ext.onReady(

function(){

  var  tree = new Ext.tree.TreePanel({

      height: 300,

      width: 400,

      animate:true,

      enableDD:true,

      containerScroll: true,

      rootVisible: false,

      frame: true,

      // getBranch.do请求服务器返回多级树形结构的JSON字符串

      loader: new Ext.tree.TreeLoader({dataUrl:'getBranch.do'}),

      root : new Ext.tree.AsyncTreeNode({id:'0',text:'根结点'}) 

      });     

      tree.expandAll();

  }

);

文件二,branchTreeJSON.jsp (接收getBranch.do请求,返回多级树形结构的JSON字符串)

<%

// 读取银行分支机构的层次数据

List result = DataAccess.getBankInfoList();

// 将层次数据转换为多叉树对象(本文下面会详细介绍该数据结构的实现方法)

Node root = ExtTreeHelper.createExtTree(result);

%>                                             

[

<%=root.toString()%> <!-- 以JSON的形式返回响应数据,Ext.tree.TreeLoader会根据此数据生成树形结构 -->

]

以上两个程序文件是一次性生成无限级树形结构所必须的,其中最为关键的部分就是如何生成一个无限级的树形结构JSON字符串,返回给客户端的Ext树形组件。对于银行分支机构来说,需要返回类似如下的JSON串:

{

  id: '100000',

  text: '廊坊银行总行',

  children: [

    {

      id: '110000',

      text: '廊坊分行',

      children: [

        {

          id: '113000',

          text: '廊坊银行开发区支行',

          leaf: true

        },

        {

          id: '112000',

          text: '廊坊银行解放道支行',

          children: [

            {

              id: '112200',

              text: '廊坊银行三大街支行',

              leaf: true

            },

            {

              id: '112100',

              text: '廊坊银行广阳道支行',

              leaf: true

            }

          ]

        },

        {

          id: '111000',

          text: '廊坊银行金光道支行',

          leaf: true

        }

      ]

    }

  ]

}

同时还需要对树中每一个层次的节点按照某一属性(比如分支机构编号)进行排序,以展示出有序的树形结构。

现在可以把问题概括为:

1、 把数据库中的层次数据转换成多级树形结构的JSON格式的字符串

2、 对树中每一个层次的节点按照某一属性(比如分支机构编号)进行排序

下面介绍解决问题的思路:

在数据结构这门课中,我们都学过树,无限级树形结构就可以抽象成一种多叉树结构,即每个节点下包含多个子节点的树形结构,首先就需要把数据库中的层次数据转换成多叉树结构的对象树,也就是构造出一棵多叉树。

有了数据结构,还要实现相应的算法,我们需要实现两种算法:

1、兄弟节点横向排序算法,对隶属于同一个父节点下面的所有直接子节点按照某一节点属性和规则进行排序,保持兄弟节点横向有序;

2、先序遍历算法,递归打印出无限级JSON字符串。

概括起来分为三步:

1、 构造无序的多叉树结构

2、 实现兄弟节点横向排序方法

3、 实现先序遍历方法,打印出JSON字符串

如图所示:


三、源代码实现(Java版)

实现这样一颗树,需要设计两个类:树类(MultiwayTree)、节点类(Node);排序时还需要一个比较器类(NodeIDComparator);为了方便演示,还需要构造一些假的层次数据,因此还需要建一个构造假数据的类(VirtualDataGenerator),以下代码拷贝出来之后可直接运行测试:

package test;

import java.util.ArrayList;

import java.util.Comparator;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;

import java.util.Set;

import java.util.Collections;

/**

* 多叉树类

*/

public class MultiwayTree {

public static void main(String[] args) {

// 读取层次数据结果集列表

List dataList = VirtualDataGenerator.getVirtualResult();

// 节点映射表,用于临时存储节点对象

HashMap nodeMap = new HashMap();

// 根节点

Node root = null;

// 将结果集存入映射表(后面将借助映射表构造多叉树)

for (Iterator it = dataList.iterator(); it.hasNext();) {

Map dataRecord = (Map) it.next();

Node node = new Node();

node.id = (String) dataRecord.get("id");

node.text = (String) dataRecord.get("text");

node.parentId = (String) dataRecord.get("parentId");

nodeMap.put(node.id, node);

}

// 构造无序的多叉树

Set entrySet = nodeMap.entrySet();

for (Iterator it = entrySet.iterator(); it.hasNext();) {

Node node = (Node) ((Map.Entry) it.next()).getValue();

if (node.parentId == null || node.parentId.equals("")) {

root = node;

} else {

((Node) nodeMap.get(node.parentId)).addChild(node);

}

}

// 输出无序的树形结构的JSON字符串

System.out.println(root);

// 对多叉树进行横向排序

root.sortChildren();

// 输出有序的树形结构的JSON字符串

System.out.println(root);

// 程序输出结果如下:

//

// 无序的树形结构(格式化后的结果,可使用JSON格式化工具查看,例如  http://jsonviewer.stack.hu/ 在线查看器): 

//  {

//  id : '100000',

//  text : '廊坊银行总行',

//  children : [

//    {

//    id : '110000',

//    text : '廊坊分行',

//    children : [

//      {

//      id : '113000',

//      text : '廊坊银行开发区支行',

//      leaf : true

//      },

//      {

//      id : '111000',

//      text : '廊坊银行金光道支行',

//      leaf : true

//      },

//      {

//      id : '112000',

//      text : '廊坊银行解放道支行',

//      children : [

//        {

//        id : '112200',

//        text : '廊坊银行三大街支行',

//        leaf : true

//        },

//        {

//        id : '112100',

//        text : '廊坊银行广阳道支行',

//        leaf : true

//        }

//      ]

//      }

//    ]

//    }

//  ]

//  }

// 有序的树形结构(格式化后的结果):

//  {

//  id : '100000',

//  text : '廊坊银行总行',

//  children : [

//    {

//    id : '110000',

//    text : '廊坊分行',

//    children : [

//      {

//      id : '111000',

//      text : '廊坊银行金光道支行',

//      leaf : true

//      },

//      {

//      id : '112000',

//      text : '廊坊银行解放道支行',

//      children : [

//        {

//        id : '112100',

//        text : '廊坊银行广阳道支行',

//        leaf : true

//        },

//        {

//        id : '112200',

//        text : '廊坊银行三大街支行',

//        leaf : true

//        }

//      ]

//      },

//      {

//      id : '113000',

//      text : '廊坊银行开发区支行',

//      leaf : true

//      }

//    ]

//    }

//  ]

//  } 

}

}

/**

* 节点类

*/

class Node {

/**

* 节点编号

*/

public String id;

/**

* 节点内容

*/

public String text;

/**

* 父节点编号

*/

public String parentId;

/**

* 孩子节点列表

*/

private List children = new ArrayList();

// 添加孩子节点

public void addChild(Node node) {

children.add(node);

}

// 先序遍历,拼接JSON字符串

public String toString() {

String result = "{" + "id : '" + id + "'" + ", text : '" + text + "'";

if (children.size() != 0) {

result += ", children : [";

for (int i = 0; i < children.size(); i++) {

result += ((Node) children.get(i)).toString() + ",";

}

result = result.substring(0, result.length() - 1);

result += "]";

} else {

result += ", leaf : true";

}

return result + "}";

}

// 兄弟节点横向排序

public void sortChildren() {

if (children.size() != 0) {

// 对本层节点进行排序(可根据不同的排序属性,传入不同的比较器,这里 传入ID比较器)

Collections.sort(children, new NodeIDComparator());

// 对每个节点的下一层节点进行排序

for (int i = 0; i < children.size(); i++) {

((Node) children.get(i)).sortChildren();

}

}

}

}

/**

* 节点比较器

*/

class NodeIDComparator implements Comparator {

// 按照节点编号比较

public int compare(Object o1, Object o2) {

int j1 = Integer.parseInt(((Node) o1).id);

int j2 = Integer.parseInt(((Node) o2).id);

return (j1 < j2 ? -1 : (j1 == j2 ? 0 : 1));

}

}

/**

* 构造虚拟的层次数据

*/

class VirtualDataGenerator {

// 构造无序的结果集列表,实际应用中,该数据应该从数据库中查询获得;

public static List getVirtualResult() {

List dataList = new ArrayList();

HashMap dataRecord1 = new HashMap();

dataRecord1.put("id", "112000");

dataRecord1.put("text", "廊坊银行解放道支行");

dataRecord1.put("parentId", "110000");

HashMap dataRecord2 = new HashMap();

dataRecord2.put("id", "112200");

dataRecord2.put("text", "廊坊银行三大街支行");

dataRecord2.put("parentId", "112000");

HashMap dataRecord3 = new HashMap();

dataRecord3.put("id", "112100");

dataRecord3.put("text", "廊坊银行广阳道支行");

dataRecord3.put("parentId", "112000");

HashMap dataRecord4 = new HashMap();

dataRecord4.put("id", "113000");

dataRecord4.put("text", "廊坊银行开发区支行");

dataRecord4.put("parentId", "110000");

HashMap dataRecord5 = new HashMap();

dataRecord5.put("id", "100000");

dataRecord5.put("text", "廊坊银行总行");

dataRecord5.put("parentId", "");

HashMap dataRecord6 = new HashMap();

dataRecord6.put("id", "110000");

dataRecord6.put("text", "廊坊分行");

dataRecord6.put("parentId", "100000");

HashMap dataRecord7 = new HashMap();

dataRecord7.put("id", "111000");

dataRecord7.put("text", "廊坊银行金光道支行");

dataRecord7.put("parentId", "110000");

dataList.add(dataRecord1);

dataList.add(dataRecord2);

dataList.add(dataRecord3);

dataList.add(dataRecord4);

dataList.add(dataRecord5);

dataList.add(dataRecord6);

dataList.add(dataRecord7);

return dataList;

}

}

好了,通过上面的代码,就可以实现多叉树的兄弟节点横向排序和先序遍历了,实现了将层次数据转换为有序无限级树形结构JSON字符串的目的。

在实际的项目中,可以把上面的有效代码融入其中,或者在此基础上进行一些扩展:

1、 实现对指定层次的排序(例如只排序第一层的节点,或者只排序某一父节点下的所有子节点)

2、 遍历输出树形结构时可以加入判断条件过滤掉某些节点

3、 实现节点的删除功能

4、 在节点类中增加一个父节点的引用,就可以计算出某一节点所处的级别

5、 在不支持层次查询的数据库应用系统中使用该算法实现相同的效果

四、思考与总结

这篇文章的重点是如何构造有序的无限级的树形结构JSON字符串,一次性生成树形结构,而不是利用Ajax的方式,反复向服务器端发送请求,一级接一级的加载树节点。

既然可以构造无限级的JSON字符串,那么也可以根据这个思路构造无限级的XML字符串,或者构造具有层次结构的UL – LI组合(用UL - LI来展示树形结构),或者构造具有层次结构的TABLE(用TABLE来展示树形结构)。如下所示:

(1)XML层次结构

<nodeGroup id="100000" name="廊坊银行总行">

<nodeGroup id="110000" name="廊坊分行">

<node id="113000" name="廊坊银行开发区支行">

</node>

<node id="111000" name="廊坊银行金光道支行">

</node>

<nodeGroup id="112000" name="廊坊银行解放道支行">

<node id="112200" name="廊坊银行三大街支行">

</node>

<node id="112100" name="廊坊银行广阳道支行">

</node>

</nodeGroup>

</nodeGroup>

</nodeGroup>

(2)UL - LI 层次结构

<ul>

<li>廊坊银行总行</li>

<ul>

<li>廊坊分行</li>

<ul>

<li>廊坊银行开发区支行</li>

<li>廊坊银行解放道支行</li>

<ul>

<li>廊坊银行三大街支行</li>

<li>廊坊银行广阳道支行</li>

</ul>

<li>廊坊银行金光道支行</li>

</ul>

</ul>

</ul>

(3)TABLE层次结构

<table>

<tr><td>廊坊银行总行</td></tr>

<tr><td>&nbsp;&nbsp;廊坊分行</td></tr>

<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行开发区支行</td></tr>

<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行解放道支行</td></tr>

<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行三大街支行</td></tr>

<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行广阳道支行</td></tr>

<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行金光道支行</td></tr>

</table>

另外对TreeGrid树形表格也有一定的价值:

1、 一次性构造树形表格,实现数据分级展示

2、 通过更换比较器,实现对不同表格列的全排序(全排序指的是对所有页的数据进行排序,而不是只对当前页的数据排序;排序规则与Oracle数据库中的层次查询类似,即兄弟节点横向排序)

3、 实现对树形表格的完整分页(每次分页时,只取固定数目的第一层节点,之后调用toString方法,展示出完整条数的分级数据,即每页的记录条数是不固定的,但必须是完整的树形结构)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容