摘要:对于树型菜单大家已经见得很多了,无论是软件中树菜单的制作还是基于WEB的树型菜单的应用,这个看似很简单的菜单在具体实现和效率方面有很大的差别,对于节点数量小,节点层数小的树型菜单来说我们用各种方法实现的结果基本差不多,但是对于层树很多,节点数量很大,关系复杂的树壮菜单而言各种实现办法之间是相差很大的,本文基于几种常用的实现方法来研究其实现方法和效率的问题。
关键字:树型菜单 效率
前言:对于网页中的树状菜单其实现结构大概可以分为以下几种结构:根节点,分支节点,叶节点,树支,对于内层结构可以分为:静态代码实现方式,异步载入模式,而异步载入模式中又可以分为:总体树载入模式和单节点载入模式,对于异步载入模式主要是出于对数据库的操作。同时对于构造树的算法方面大致最常用的也就是:父子节点编号法,和前缀码编号法,而节点的传输方面又可以分为:服务器端动态生成HTML方式,服务器端静态生成HTML方式,客户端保存HTML方式,可以看到这里实现方法很多,也很复杂,所以本文仅仅研究了几种比较成功的例子,虽然部分代码和程序来源于网络,但我还是具体实现了一种利用APPLET方式生成动态树,并且是单节点刷新的方法,而且能重用,具体实现思想和设计模式将在本文最后的实例分析中可以看到。
正文:
1.理论分析:
1.1语言实现方法分析:各种实现方法之间由于所在的平台不同可能表现出的优越性不同,但是其本质,也就是编译方式和解释方式决定了其效率的高低,下面我们比较几种实现方式的具体差异,从最低层来看看他们本应该存在的差异。
1.1.1 Javascript
运行原理:作为一种脚本语言,用于实现HTML网页中的动态操作,其运行原理和一般程序不一样,而是在程序运行的过程中被逐行解释的,同时它是以事件驱动的方式完成对事件的处理。
特点及其局限性:跨平台可以说是它最大的特点,给它带来优点的同时也带来了很多局限性,比如:游览器的局限性和安全性有关的局限性,但是因为它和HTML很好的结合模式,从而也导致了它是应用很广的一门脚本语言。
1.1.2 Java-Applet
运行原理:属于嵌入到游览器环境中的程序,必须由游览器的虚拟机(JVM)负责执行。当在本地编译完成以后,生成字节码文件,这样我们就可以通过导入字节码文件的方式,来实现我们的操作。
特点及其局限性:和Javascript一样也有垮平台性,但是我们只需要对其编译完以后用户就可以很方便的实现其功能,同时因为是java的一种嵌入式开发模式,我们可以很方便的利用SWING组件和其中的类,这样会给我来很高的效率。
1.2数据加载模式分析:具体各以分为一次性加载数据模式和异步加载模式。
1.2.1 异步加载模式:事实上就是实现单节点的刷新模式,当用户点击某个节点的同时获得这个节点的信息,这样我们就可以通信服务器,从其数据库或者XML文件中读取子节点信息,并且加载到页面,完成显示工作。
1.2.2 异步加载的优点:对于用户的一次操作我们需要通过传输的数据量很少,同时支持我们只对数据库进行操作时,比如:当用户没有点击某个节点的时候,如果这个时候对数据库中的数据更新,这样用户的到的也面效果也是最新的。其无限扩展性也是很好的,我们只需要修改我们的数据库或者XML文件,添加和删除节点,即使是数量很大也没有关系,对于用户的显示也只是通信时间上的问题。
1.2.3 异步加载的缺点:设计模式比数据一次性加载要复杂得多,要考虑到 B/S 之间的应答,要判断子节点是否含有孙节点,后台数据源的层级关系模型等。对网络传输的信赖性太大,每个节点的展开都需要连一次服务器,只要在取某节点数据时网络出现问题,就会导致该节点及其以下的子节点加载失败,同时对于服务器的通信次数太多(具体用户操作而言),这样会加重服务器的负担,所以这也是一个很大的缺点。
1.2.4 一次性加载模式:相比上面这种数据加载模式而言,一次性加载模式是当用户获得网页后一次性从服务器把整个树的信息加载到游览器上面,虽然我们没有打开某些节点,但是我们事实上在本地已经获得了这些节点的信息,只是我们并没有展开父节点看到它而已。
1.2.5 一次性加载的优点:相比异步模式,它的层次结构就更加清晰,实现方法也简单很多,。而采取数据一次加载的模式只要一次加载成功,服务器就可以不用管它了,服务器压力减轻,脚本设计则完全独立,对整棵树节点的检索可以在客户端完成,节点展开响应速度快等等优势,因此在节点数不多的情况下数据一次性加载更有优势。
1.2.6 一次性加载的缺点:虽然对于服务器通信的次数很少,但是在第一次获得节点信息的时候数据量可能会很大,可能会由于网络环境的影响而严重影响用户的界面访问,对于节点总体节点信息量很大的情况不适合,同时对于服务器端的实时更新我们必须重新加载一次才能访问更新的内容。
1.3节点信息传输问题:在浏览器里显示的树结构其实都是一个个 HTML 元素组合起来的,在 WEB 页面里的树都是根据树节点的信息组合成一串的 HTML 元素列来显示,这一步从节点信息到 HTML 的转化可以在两个地方生成:一个是在服务器端,一个是在客户端。
1.3.1 服务器端动态生成HTML方式:在服务器端生成的优点在于不须考虑客户端的浏览器的兼容性,节点信息的可控性非常强,但是它的缺点也是非常大的:加重服务器的负担,增加网络传输量。在服务器端直接生成树节点的 HTML 给服务器带来的压力是显而易见的,也是非常巨大的,估计只要有几十个并发连接就能让服务器暂时停止响应了。
1.3.2 服务器端静态生成HTML方式:当然可以直接将树生成为一个静态文件放在服务器端,这种做法对于树节点相对固定不变的情况还是非常有利的,只需要生成一次,以后就不需要再生成了,服务器的压力也非常小,但它的弊病在于可变化性太小,比如说不同的权限能看到的树节点的不同这种情况,用这种生成静态树放在服务器端的做就没有办法解决。
1.3.3 客户端保存HTML方式:这样虽然解决了传输量大的问题,但是当节点信息量很大时,我们在客户端本地保存的信息也是很大,可能会占用大量的本地的硬盘资源,并且每次的访问是独立的一次,当我们的信息量很大,而且用户的访问次数很多时,我们使得用户的负担加重。
1.4树结构实现算法分析:
1.4.1 父子节点编码法:我们可以很容易就想到链表中的指针的概念,这种编码方法也是基于这种思想,当我们的每个节点都有唯一的ID号的时候,我们在描叙一个树节点的时候,只在储存其节点ID的同时储存其父节点的ID,这样我们就可以很容易通过寻找父节点的方法实现一棵树的建立。
1.4.2 编码实例:
ID号 | 父节点ID |
1 | 1 |
2 | 1 |
3 | 1 |
4 | 3 |
5 | 1 |
6 | 5 |
搜索成的树型图:(图 1.3.2 .A)
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
1.4.3 编码的优点:我们可以很清晰的看出每个父子节点的信息,所需要的数据结构和数据库设计也很简单,很便于节点数量少的树型菜单设计方式,即使是对于层次复杂的树型结构我们也只对单个节点描叙ID和父节点ID的信息。
1.4.4 编码的缺点:对于数据库的搜索模式事实上也是采取的逐行扫描的方法实现的,这样我们如果采用这种编码方式的话,如果在访问数据库获得数据库内容的时候,如果我们没有进行排序操作的话,我们显示的节点信息可能会有很大的差别,请看下图( 1.3.4 .A),同时我们在搜索的时候可以采用两种搜索方法,第一种:我们按ID号大小分别作为父节点,分别搜索其子节点,这样的效率肯定不高,搜索的次数可能会很多,对数据库的访问次数也很多。第二种,我们采用递归搜索的方法,当搜索到一个节点的父节点的ID时,我们再把这个ID号作为子节点的ID号来进行搜索,从而利用递归的方法实现树型结构,但是这样的效率上和实现难度上有很高的要求。
搜索结果不同:(图 1.3.4 .A)
+ 1 + 1
+ 2 + 3
+ 3 + 2
+ 4 + 4
+ 5 + 5
+ 6 + 6
没有互换之前的搜索兔 互换后的搜索图
*说明:我们把节点号为2和3的节点在数据库中互换位置,得到不同的结果,虽然这样一个节点的子节点个数和相关信息没有变化,但是对于自身在父节点中的位置也发生了变化,对于某些需要具体顺序的应用将产生很大影响。
1.4.1 前缀码编码法:我们可以很容易用数字描叙树结构中的层次关系和顺序关系,例如:0代表根节点,0.1代表其下的第一个子节点,利用这种编码方法,我们很容易就可以描叙一棵树形结构。
1.4.2 编码的优点:在进行建立树结构的过程中我们很容易就可以找到其子节点的信息,只要以父节点的ID号作为前缀码,按顺序获得两个分割符之间的数字,这个数字事实上就是以这个父节点为根的子节点的索引号,这样我们可以把每个父节点看成一棵树的根,子节点的索引也就是在这个树下面的节点编号。
1.4.2 编码的缺点:对于层次数量比较大的树型菜单我们在数据库中描叙其层次结构时很复杂,因为是前缀编码方法,所以不利于手动修改数据库信息,同时对于网络传输中,我们如果没有对数据进行压缩,这样我们的节点ID号方面的数据量会因为我们的层次数量的增大而增大。
编码例子:
ID |
0 |
0.1 |
0.2 |
0.2.1 |
0.3 |
0.3.1 |
搜索结果:
+ 0
+ 0.1
+ 0.2
+ 0.2.1
+ 0.3
+ 0.3.1
2.具体实现方法:
由于涉及的语言方面的实现方法很多,本文只对网上比较流行的实现方法HTML结合javascript的方法还有java-applet实现方法进行了分析,同时对于各种软件制作树状菜单的原理也进行了分析。
2.1 HTML-javascript实现方法:我们利用HTML完成对节点信息的描叙,这完全是静态的添加HTML代码的操作,同时用利用javascript脚本来实现对节点的事件处理。
2.1.1 静态实现代码:(对应于代码一)
<!DOCTYPE HTML PUBLIC "-//W 3C //DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title></title>
<link type="text/css" href="css/tree.css" rel="stylesheet">
<script language=javascript>
function ChangeStatus(o)
{
if (o.parentNode)
{
if (o.parentNode.className == "Opened")
{
o.parentNode.className = "Closed";
}
else
{
o.parentNode.className = "Opened";
}
}
}
</script>
</head>
<body>
<div class="TreeMenu" id="CategoryTree">
<h4>CSS树形菜单</h4>
<ul>
<li class="Opened"><img class=s src="css/s.gif" οnclick="javascript:ChangeStatus(this);"><a href="#">根节点</a>
<ul>
<li class="Opened"><img class=s src="css/s.gif" οnclick="javascript:ChangeStatus(this);"><a href="#">我的文档</a>
<ul>
<li class="Opened"><img class=s src="css/s.gif" οnclick="javascript:ChangeStatus(this);"><a href="#">JavaScript</a>
<ul>
<li class="Child"><img class=s src="css/s.gif"><a href="#">1</a></li>
<li class="Child"><img class=s src="css/s.gif"><a href="#">2</a></li>
<li class="Child"><img class=s src="css/s.gif"><a href="#">3</a></li>
<li class="Child"><img class=s src="css/s.gif"><a href="#">4</a></li>
<li class="Child"><img class=s src="css/s.gif"><a href="#">5</a></li>
<li class="Child"><img class=s src="css/s.gif"><a href="#">6</a></li>
<li class="Child"><img class=s src="css/s.gif"><a href="#">生成node</a></li>
</ul></li>
</ul>
</div>
</body>
</html>
2.1.2 代码功能分析:对于各个节点的操作控制我们用下面的脚本:
<liclass="Opened"><imgclass=ssrc="css/s.gif"οnclick="javascript:ChangeStatus(this);"><a href="#">根节点</a> 进行控制,对于每个分支节点我们都需要添加这条,对于各个父节点的子节点的数量我们必须通过添加如下代码实现,<li class="Child"><img class=s src="css/s.gif"><a href="#">1</a></li>,这样我们就可以实现HTML的静态显示,用javascript来实现事件控制,对于图片加载还需要说明的是,我们看到的节点的图片是加载进去的,同时对于连线的效果也是加载进去的,这也是这次研究的一个发现,对于这个代码如何处理事件是利用函数ChangeStatus来控制的。
2.1.3 代码优缺点分析:代码很简洁明了,对于实现层次结构不复杂,节点数量不多的树型菜单的时候很方便就可以实现功能,避免了对数据库的访问,这样使得服务器的负担减轻。但是我们每次都是手动的去添加节点的信息,这样会造成效率很低,而且整个HTML文件的数量也很大。
2.2 HTML-javascript动态加载实现方法:(说明:这个实例是引用网友“meizz”提供的MzTreeView1.0,其控件被CSDN论坛进行修改,而作为其论坛的树状导航菜单)这里的动态加载实现方法事实上就是从数据库中获得相关的节点信息,再构造出树结构。
2.2.1 实现代码:略.(对应于文件夹代码二)
2.2.2 设计思想分析:
A.采用数据一次性加载:把获得的树信息一次性加载给用户,减轻了服务器的负担和访问次数。
B.节点信息的压缩传输:节约了在传送节点信息的HTML文件的时候文件过于庞大。
C.数据库设计:采用父子节点编码方法,结构简单,清晰,同时为每个节点添加了对应的脚本操作,这样节省了打开各个节点时的时间。
D.异步展示:对于整个节点的HTML文件的生成不是以整体的方式展示在用户面前,而是以单单要显示的页面而生成其HTML。
E.采用文字树线:这样避免了对于图片树线的加载,使得展开速度更加快。
F.控件的扩展性:控件的扩展性和重用性都比较高。
G.其他优点由于本人知识范围有限无法获得。
2.3 java-applet实现方法(对应于文件夹代码三):对于applet这种嵌入式开发模式我们可以利用java自身包含大量很方便的类和接口完成生成树的操作。本实例子是通过对比meizz网友的设计模式重新开发的一种,动态实现单节点刷新的树状菜单生成方法,主要用到了JTree接口和相关的类。
(*说明:本程序全是通过本人编写而成,目的是对比不同的数据库设计方法,和不同的加载模式对树型菜单效率的影响,之所以采用这些模式,主要是和上面的一个实例进行对比。)
2.3.1 实现方法:
2.3.1 .1数据库设计:
A.采用前缀码编码方式,属性为NodeID,并且为主键。
B.采用限制搜索次数的限制,考虑到形成树的时候如果采取递归搜索方式,这样就会造成效率比较低,所以为每个节点设置了另外一个属性Childnum,目的是加快数据库的搜索速度,如果不用这种方式,数据库扫描搜索的次数将扫描所有的节点,这样我们只需要扫描Childnum次就可以得到子节点的信息。
C.采用前缀码搜索,以父节点的ID号作为每次搜索字符串的前缀,我们只需要再添加一个‘.’符号并改变响应的尾数,这样我们就可以得到子节点的信息。
D.利用异步刷新模式,动态的载入用户点击的节点的子节点信息。
E.采用判断文件夹型节点设计方法,对于我们的节点可以分为根节点,和分支节点我把其Folder属性设置为1,这样我们就可以判断得到这个节点是否是根节点或者是分支节点,如果是我们就加载子节点,如果不是就不用加载,进一不减少了对数据库的操作。
F.设置节点是否已打开过属性,当我们发现一个节点的Opened值为0时,我们就需要对起节点添加节点,如果为1,说明我们不需要在加载子节点了,当然这样的操作前已经对其Folder值进行了判断,这样我们又节省了对数据库的操作。
G.添加了文本,图标,和链接的信息,属性为Text,Icon,URL。
数据库属性图( 2.3.1 .1.A):
数据库内容图( 2.3.1 .1.B):
2.3.1 .2编程难点和解决方法:
A.通过点击节点获得该节点的编码信息。我们虽然可以很容易的到我们点击的节点事件,只需要implements TreeSelectionListener就可以很容易得到我们的节点触发事件,但是我们只是得到一个节点的实例,而并不是得到其应该有的编码方式,并且这个编码必须是在程序里面利用类的方法实现的,而不是在数据库里面实现,由于树节点和树结构相关的类很多,通过反复的比较和尝试最后选定用递归编码的方式实现,但是这里还是列举了我在思考中想到的办法。
解决办法:
方法一:逐步搜索法:从根节点开始一个一个搜索并且编码,和得到的树节点的事例进行比较,这样我们可以得到这个节点的编码,但是效率实在是很低。而且没有很好的利用各个类的方法。
方法二:递归编码法:得到这个实例后查看这个节点的父节点是否为空,如果为空说明是根节点,如果不为空,利用getIndex得到这个节点以其父节点为根的树的节点中的索引,把这个索引添加到编码字符串的前面,再以其父节点作为子节点继续递归编码,当搜索到根节点的时候直接添加0到最前端这样就实现了编码,具体编码程序如下:
//方法说明: 找到编码的结果,利用递归寻找
//参数说明: DefaultMutableTreeNode tempnode 为该节点的实例
// String save保存编码的结果的字符串
//返回值说明:返回这个节点的编号
public String codeing(DefaultMutableTreeNode tempnode,String save)
{
if (tempnode.getParent() != null) //如果不为空的话,说明有父节点
{
DefaultMutableTreeNode nodeparent = (DefaultMutableTreeNode)tempnode.getParent(); //得到parent信息
int i = nodeparent.getIndex((TreeNode)tempnode); //得到在其父节点下的INDEX号
i=i+1; //
save ="."+i+ save;
return codeing(nodeparent,save);
}
else
{
return "0"+save; //说明是树的根节点
}
}
B.子节点搜索问题,我们要多每个父节点的子节点进行搜索,这样我们每次都需要改变搜索的语句,这样得到的才是我们需要的子节点,我想过用LIKE这种SQL语句但是,由于可能有误差,例如:以0为前缀,我们需要得到和0.i相同的节点,如果只用LIKE 0.i%来表示的话, 0.1.1 和0.1都在搜索后的结果集里面,这样我们不利于区分,同时我也想过用判断字符长度的办法,但是由于0.1.1和 0.100的字符长度相同这样也得不到正确的结果,所以我就采用了逐个搜索的方式,设计Childnum属性的原因也在这里,只要改变每次搜索字符串的形式就可以了,也就是改变了变化不大的SQL语句,具体实现方法如下。
//方法功能: 得到一个子女节点
//参数说明: ID为这个父节点的ID号
// index为这个子节点在以这个父节点为根的树中的索引号
//返回值说明:String返回值为这个节点的ID号
public String getonechildID(String ID,long index)
{
String temp = new String(""); //临时变量
try
{
rs = sql.executeQuery("SELECT * FROM NodeInfo WHERE NodeID = '"+ ID+ "."+index+"'");
while(rs.next())
{
temp = rs.getString("NodeID");//得到该节点的信息
}
}
catch(SQLException e3)
{
System.out.println("Exception6");
}
return temp; //返回这个子节点的信息
}
C.关于Date类的使用,因为要测试产生节点的时间,比较各种方法的效率问题,所以在我的程序中设计了这个类来获取产生节点的时间,但是由于Date类,在java.util和java.sql两个包中都有,而且这个程序是和数据库相连的程序,所以这两个包必须都引入,但是在定义的时候会出错,通过上网查找我们必须在定义时这样定义:java.sql.Date then,指明用这个包的类来定义一个实例。
D.其他小问题,对于异常的检测办法还是不是很了解,其他的基本都没有问题了。
3.1对相关网站树型菜单的事例研究:
3.1.1 水木清华BBS论坛:
网站地址:http://www.smth.edu.cn/frames.php
网站树型菜单结构图:
略
网站树型菜实现途径分析:通过对这个BBS的树型菜单的代码和运行情况分析,我们可以从代码中发现对于部分分支节点(数量比较小)采用的是一次性通过HTML文件加载方式实现的,可以说是静态载入的过程,但是对于数量比较大的分支节点是采用数据库异步加载的方式实现的,从这可以看出菜单的具体实现不一定总是限制于一种方法,可以是两种方法结合的特点。
4.实现方法效率比较:
4.1测试环境:
A.操作系统:winXP Professional (sp2)
B.CPU:Intel (R) Pentium(R) processor 1500HMz 主频 598HMz
C.内存: 768M 266HMz
D.显卡:ATI MOBILITY RADEON 7500 32M
E.游览器: IE 6.0
F.测试项目:静态实现10000个菜单的时间
4.2javascript静态实现(对应于文件夹代码四),for循环创建树节点。
A.重用性:不利于重用
B.运行时间:2243毫秒
C.代码来源:CSDN网站网友
D.分析:相比较其他javascript静态生成方法运行效率很高。
4.3applet静态实现(对应于文件夹代码五),for循环创建树节点。
A.重用性:作为单独利用APPLET技术嵌入网页,可以重用。
B.运行时间:130毫秒
C.代码来源:自己编写
D.分析:运行效率很高。
4.4对上面两中结果的分析:由于javascript是用逐步运行解释的原理进行编译的,所以效率比较底下,而applet实现的是用游览器载入字节码文件,只需要运行就可以,不需要再在客户端编译,而只需要用客户端的虚拟机运行就可以了,所以效率相对于javascript而言效率就比较高,即使是静态的插入节点也很方便应用于网页树型菜单的实现,但是由于javascript是和HTML在一起实现这个功能对于具体的操作系统我们很容易实现对节点连接信息的网页跳转,但是对于applet我们需要运用底层操作实现具体某个游览器的跳转,这也是为什么applet实现树菜单运用这么少的真正原因,因为我们的客户端操作系统大多是微软的WINDOWS系列。
4.5测试说明:我测试的都是静态添加节点的操作所需要的时间,没有进行对数据库载入节点的程序进行测试,可以通过编写程序在数据库中插入10000个节点再进行测试,但是由于时间关系,没有进行编写,但在以后的一段时间会完成这个测试,测试的程序是本文两个和数据库连接的实例。
5.参考文献:
[1] 耿祥义.java大学实用教程 北京:电子工业出版社,2005
[2] 胡恒.javascript网页开发实例教程 北京: 机械工业出版社,2003
[3] 谢若阳.数据结构 北京:清华大学出版社,2005
[4] 周亚辉.数据库系统项目开发实践 北京: 科学出版社 2005
[5] 耿祥义.jsp基础教程 北京:清华大学出版社,2005
[6] 陈志泊 李冬梅 王春玲.数据库原理及应用教程 北京:人民邮电出版社
[7] Kathy Walrath 等.JFC SWING标准教程 北京:电子工业出版社