在web开发中的树状视图技术
树型数据模型在现实生活中应用相当广泛,从超市的商品分类到政府的组织结构无不都是”树形”的。在实际的项目开发中也经常需要显示这种结构。比如,在树状视图上给一个单位的职工赋予系统操作权限。
在web开发中实现树状显示在技术上主要有以下两种方案
1. 采用js操作DOM模型构建树状视图
2. 采用HTML来表达树的节点,使用js和css来赋予动态效果
第一种方案的典型示例是网上流传的各种tree.js脚本。例如:http://www.blueshoes.org/en/javascript/tree/
这种方式在运行时由客户端浏览器动态生成树状结构。特点是操作比较简单,对二次开发人员来讲上手比较容易。缺点是js脚本运行速度缓慢。在一些需要和用户进行“强交互”的场景下而树的节点又比较多(比如上万个),每次浏览器刷新后需要”漫长”的等待来生成DOM模型,会让客户觉得不友好。
另外一个缺陷就是在树状视图上进行操作,例如在树型视图的节点上使用鼠标单双击触发动作或是生成菜单。实现这些必须要深入到js脚本中的细节中才可以订制。对开发人员来讲难度非常大。
第二种方案的特点是使用HTML元素静态表达树状节点,使用js和css样式来赋予动态效果,在HTML节点上实现菜单和鼠标触发动作。如下图所示:
大家知道浏览器渲染静态HTML数据的速度是非常快的,几百K的静态数据在浏览器里的渲染基本上就是一瞬间。在需要频繁刷新视图的情况下也基本上感觉不到延迟,甚至可以实现CS开发中”操作结果立即可见”的效果。在静态HTML元素上设置触发动作和菜单也比较方便,给开发人员自由发挥的余地较多。
这种方案的缺点是技术难度较大编码比较复杂,需要使用框架进行规范实现来克服这种弊病。
webTreeViewer框架
webTreeViewer框架可以根据数据库到树状模型的映射配置信息直接生成树状视图。用户唯一需要做的就是编写由数据库到树状数据模型的映射文件并配置到框架中去,再通过框架预定义的标签来指定视图的具体显示效果、设置菜单等。编写非常方便,基本上为二次开发人员屏蔽了所有的技术细节。二次开发人员只需要告诉框架”想要做什么”。
视图的显示速度非常快。原公司开发的项目里,在一台配置较低的商用机中同时运行ORACLE,原公司项目及客户端浏览器,在节点数达到2000个的情况下刷新视图基本感觉不到延迟。
webTreeViewer依赖HQL语言,但不是非得依赖Hibernate框架,但目前来看Hibernate是最好的选择,也可以移植到纯JDBC项目或其他持久化技术下。 webTreeViewer采用spring进行配置并作为IOC容器,但并没有同spring进行耦合。所有的配置完全可以采用手动编码来实现。
为了更好的控制js脚本和html,框架使用了freeMarker,并且使用apache –degister来解析配置项。
在视图技术上采用一些列jsp标签来具体设定显示效果。
视图依赖的所有资源包括class文件、图片、脚本等都集成在jar包中,在需要的时候框架会自动加载,不需要再引入其他文件。
框架使用介绍
基本工作原理简介
webTreeViewer框架的设计是受了SWT/JFACE treeViewer启发的,所以熟悉JFACE treeViewer的朋友对webTreeViewer应该可以很快上手。
同大部分视图技术一样, webTreeViewer也使用了MVC模型。
在模型层要做的工作就是将”树状的”存储结构映射为”树状”的数据结构。此步骤由框架操作HQL wrapper来完成.所谓HQL Wrapper就是可以执行HQL语句进行数据库查询的实例,实现I_HQLWrapper接口。
树的数据结构类似于普通的面向对象的树状数据结构。可以导航到父节点,对于子节点的集合引用等。每一个节点都有三个强制性的属性即id、layer和作为类别标识的category。另外还可以设定扩展属性properties。 Properties是一个map的结构,可以映射一个键值对,用户可以将任何存储结构中的属性映射进来。每一次需要显示时,树形结构的根节点都会被传送到视图层。在jsp中可以通过EL、ONGL、标签或其他技术来取得map中的值显示在视图。
webTreeViewer有默认的机制可以把对树状显示的要求自动转发到提取数据的模块中,生成模型之后再自动转发到用户预定义的jsp文件,并最终显示在客户端。这一切只需要”配置”即可,除了jsp页之外不需要写一行代码。对于高级用户,框架也预留了足够的灵活性。在控制层,用户可以自己手动的提取模型数据,根据自己的要求对模型数据进行修改,再手动转发到视图。可以使用任何熟悉的web框架及技术,比如struts。当然这需要额外的代码工作才能完成。
数据模型是在一个TreeViewContext 中交给视图的。在视图上树状数据模型被遍历迭代,每一个节点都要迭代给用户以决定其具体的显示效果。同SWT/JFACE treeViewer的类似,你可以设置一个过滤器和排序器来决定哪些节点最终被留下来,以及他们在一个父节点下的显示顺序如何。 webTreeViewer框架使用了跟多技术来处理视图层,比如包括对菜单的处理等,可以让HTML代码很好的同js脚本相结合,让视图的开发编写简洁高效。
视图上的一个动作被触发之后,比如注册在菜单上动作或是自定义的鼠标单击事件,都有可能刷新数据模型(数据库中的存储模型),这时可以选择刷新视图来更新显示。上面的每一个步骤将会重演,将最新的数据库信息映射成树状显示返回给浏览器;同时,客户端框架会保存之前的”状态”,比如客户打开了哪些节点,当前节点在整个视图的位置即滚动条的位置等都会被恢复。这一切都会在一瞬间完成,可以很好的模拟CS开发的效果。如果节点较多,比如有几千个甚至过万,而且服务器的效率不高时可能会有一些延迟和闪烁的问题。在下一个版本中准备采取一个类似于”双缓冲”的技术解决客户端闪烁的问题。
总而言之,我已经尽力屏蔽了所有技术细节,webTreeViewer可以做所有的这些工作,二次开发人员只需要告诉它”到底要做什么”。
下面就举一个具体实例来演示说明,实例代码可在此下载。
1. 树状模型和映射文件的编写
在模型层,框架做的基本工作就是将树状的存储结构映射为树状的数据结构。
比如,我们要做一个图书馆的项目。项目要求在浏览器中按照树状结构来展示图书,图书都是属于某个类别的,比如武侠类图书。
我们在MYSQL数据库中创建如下表:
create table SORT
( ID bigint not null auto_increment,
PARENT_ID bigint,
LAYER smallint not null,
NAME varchar(60) not null,
DISCRIPTION varchar(200),
IMAGE varchar(200),
primary key (ID)
)type = InnoDB;
alter table SORT add constraint FK_SELF_CON foreign key (PARENT_ID)
references SORT (ID) on delete restrict on update restrict;
注意到这个表是”自关联”的。任何树状存储结构的表结构都应该是自关联的,否则如何表达树形关系?在这里”类别”也是自关联的,比如武侠类属于通俗文学类,而通俗文学又属于文史类。
存储结构中以自关联形式创建的表称为树的”branch”,即树干的意思。
光有树干(树枝)还不行,还要有树叶才可以成为一棵完整的树。所有以外键引用”树干”的表结构称为leaf---树叶,而且leaf可以不只一种。
在此例中图书馆在具体的类别下面保存着两种东西——— 图书和光盘(VCD)
create table VCD
( VCD_ID bigint not null auto_increment,
TREE_ID bigint,
VCD_NAME varchar(60) not null,
primary key (VCD_ID)
)type = InnoDB;
alter table VCD add constraint FK_VCD_TREE foreign key (TREE_ID)
references SORT (ID) on delete restrict on update restrict
图书表同VCD表类似,具体结构参照实例代码
下面一点比较重要的。在框架的数据结构中只有一个类即mx.web.treeView.modelLayer.TreeModel。数据的branch和leaf最后都要被映射为TreeModel.。TreeModel的id,即具体数据行的主键值是一个long型值(一般来讲代理主键都是long型的);ayer表示这个节点距离根节点一共有几层,对于由leaf映射而成的TreeModel来讲它总要比挂接它branch多一层;category在这个例子中对于图书类别表就是”sort”,对于图书就是”book”,光盘就是”vcd”。
框架通过映射文件来生成查询语句,而且只使用的投影查询。只有在映射文件里定义的属性才会最终出现在SQL中。
映射文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<treeNode-mapping>
<branch-node class="mx.sample.mappings.TreeNode"> ----hibernate实体bean的类名
<id>id</id> ----实体bean中代表主键的属性
<category>sort</category> --- 在此直接设定类别值
<childrenNode>treeNodes</childrenNode> ---- 对子branch的引用属性
<layer>layer</layer> ---- 代表层数的属性
<extends>
<extend>
<name>name</name>
<field>name</field>
</extend>
<extend>
<name>discription</name>
<field>discription</field>
</extend>
<extend>
<name>image</name>
<field>image</field>
</extend>
</extends>
</branch-node>
<leafs>
<leaf-node class="mx.sample.mappings.Book">
<associate-field>treeNode</associate-field> ----引用branch的javaBean的属性
<id>bookId</id>
<category>book</category>
<extends>
<extend>
<name>bookName</name>
<field>bookName</field>
</extend>
</extends>
</leaf-node>
<leaf-node class="mx.sample.mappings.Vcd">
<associate-field>treeNode</associate-field>
<id>vcdId</id>
<category>vcd</category>
<extends>
<extend>
<name>vcdName</name>
<field>vcdName</field>
</extend>
</extends>
</leaf-node>
</leafs>
</treeNode-mapping>
注意到无论是branch还是leaf,都定义了扩展属性。其中name是jsp页面中在treeNode的properties(map<String,Object>类型)绑定的key,而field是在hibernate中代表这个域的javaBean属性,根据规范每一个属性通常都有setter和getter。
category元素中直接将值写入,而不是映射的属性名。
在此文件中的所有配置都是hibernate实体bean中的域名而不是数据库中数据的字段名。
框架根据映射文件来对数据库进行查询,提取需要的字段值并组装成数据模型实例。感兴趣的朋友可以检阅hibernate日志查看框架自动生成的sql语句,这里就不再赘述。
2. 使用配置文件配置web层或自定义web层
除了映射文件之外,还有一个properties文件,用于设置”一棵树”的具体显示样式,比如branch的图表,一些样式等.目前这个功能暂时没有实现,所有的样式用的都是默认的样式.
在web.xml中添加如下配置
<servlet>
<servlet-name>treeServlet</servlet-name>
<servlet-class>mx.web.treeView.TreeServlet</servlet-class>
<init-param>
<param-name>treeRegisterLoader</param-name>
<param-value>mx.web.treeView.SpringLoader</param-value>
</init-param>
<init-param>
<param-name>url-segment</param-name>
<param-value>treeServlet</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>treeServlet</servlet-name>
<url-pattern>/treeServlet/*</url-pattern>
</servlet-mapping>
treeServlet有两个作用
1. web容器启动的时候,启动配置在spring里的webTreeViewer加载所有的配置项。
2. 作为默认的web层,负责执行生树状数据模型的业务逻辑,并转发到默认视图。
spring配置文件如下(详细配置请查看演示文档):
<bean name="treeRegister" class="mx.web.treeView.TreeRegister"
abstract="false" singleton="true" lazy-init="default"
autowire="default" dependency-check="default">
<property name="treeConfigMap">
<map>
<!-- entry key="treeOne">
<ref bean="treeConfigOne" />
</entry-->
<entry key="treeTwo">
<ref bean="treeConfigTwo" />
</entry>
<!-- entry key="treeThree">
<ref bean="treeConfigThree" />
</entry -->
</map>
</property>
<property name="treeDispatchUrl">
<map>
<entry key="treeOne" value="/tree/One"></entry>
<entry key="treeTwo" value="/treeSample/treeSample.jsp"></entry>
</map>
</property>
</bean>
<bean id="treeConfigOne" class="mx.web.treeView.TreeConfig"
singleton="true">
</bean>
<bean id="treeConfigTwo" class="mx.web.treeView.TreeConfig"
abstract="false" singleton="true" lazy-init="default"
autowire="default" dependency-check="default">
<property name="propertiesFile">
<value>
WEB-INF/classes/mx/sample/resources/treeView.properties
</value>
</property>
<property name="mappingFile">
<value>
WEB-INF/classes/mx/sample/resources/treeView-mapping.xml
</value>
</property>
<property name="treeService">
<ref bean="treeServiceProxy" />
</property>
</bean>
每一棵树都要使用TreeConfig类进行配置。首先需要设置的是属性文件及映射文件的位置。在此例中,这两个文件在classpath根路径上的一个文件夹中,设置的时候却要按照项目根目录的相对路径来设置,这样的好处是文件可以放在项目的任意位置。
treeService是一个算法类,其需要注入一个hqlWrapper。它的主要作用是生成HQL语句,并从数据库的查询结果中生成树状数据模型。
TreeConfig必须注册在treeRegister中去。在spring配置文件中,必须使用“treeRegister”这个名字作为绑定名使得springLoader可以找到它。在treeRegister中还要设置每一棵树默认的jsp页面。
web项目启动的时候springloader会加载这些配置项。当然,也可以自己手动生成TreeConfig实例再注册到treeRegister中去,最后再手动设置给treeServlet。框架预留了自由发挥的余地,以适应不同项目的需要。你可以使用自己熟悉的web技术,比如struts来充当web层。这样在struts的代码中,就需要手动调用treeService生成树状模型,然后根据自己的需要遍历、修改,最后再转发到自定义的视图上。
3. 使用jsp标签实现视图并设置菜单
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://mx.web.views/tree" prefix="mx"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<script language="javascript" type="text/javascript">
function add_folder(elementId){
alert("add_folder "+elementId);
}
function modify_folder(elementId){
alert("modify_folder "+elementId);
}
function delete_folder(elementId){
alert("delete_folder "+elementId);
}
</script>
</head>
<body>
<mx:tree style="font: 14px;">
<mx:treeMenu debug="false" offset="40,10">
<table width="100"
style="font: normal 12px; background: #efefef; border: 1px solid #666666;">
<tr style="cursor: hand">
<!-- @operation(add_folder) -->
<td onMouseOver="this.style.backgroundColor='#3982F7'"
align="center" onmouseout="this.style.backgroundColor='#efefef'">
添加文件夹
</td>
</tr>
<tr style="cursor: hand">
<!--@operation(modify_folder) -->
<td onMouseOver="this.style.backgroundColor='#3982F7'"
align="center" onmouseout="this.style.backgroundColor='#efefef'">
修改文件夹
</td>
</tr>
<tr style="cursor: hand">
<!-- @operation (delete_folder)-->
<td onMouseOver="this.style.backgroundColor='#3982F7'"
align="center" onmouseout="this.style.backgroundColor='#efefef'">
删除文件夹
</td>
</tr>
</table>
</mx:treeMenu>
<mx:treeIterator elementName="element">
<mx:treeMenu_operation>
add_folder , modify_folder, delete_folder
</mx:treeMenu_operation>
<mx:treeItem_img>
<c:if test="${element.category=='book'}">
<img src="<%=request.getContextPath()%>/treeSample/image/book.gif">
</c:if>
<c:if test="${element.category=='vcd'}">
<img src="<%=request.getContextPath()%>/treeSample/image/vcd.gif">
</c:if>
</mx:treeItem_img>
<mx:TreeItem_str>
<span style="vertical-align:12%;">
<c:if test="${element.category=='sort'}">
${element.properties["name"]}
</c:if>
<c:if test="${element.category=='book'}">
${element.properties["bookName"]}
</c:if>
<c:if test="${element.category=='vcd'}">
${element.properties["vcdName"]}
</c:if>
</span>
</mx:TreeItem_str>
</mx:treeIterator>
</mx:tree>
</body>
</html>
在jsp页中主要使用标签来具体设置树状视图的显示样式和菜单。通过查看浏览器端的源文件可以知道,生成的html代码同jsp页之间的差别是较大的,实际上框架做了很多工作,比如生成js脚本,规范在html中树节点的代码缩进,处理一些特殊字符,解决在GBK和UTF-8编码之间的显示差异等。
树形标签必须按照上面的示例正确嵌套
1. < treeMenu >
在treeMenu中设置菜单。在这个元素内可以设置一个完全由用户订制的菜单控件。在这个例子中使用了<table>,当然还可以使用其他的标签,比如<div>。菜单的显示完全由html订制。在用户使用鼠标右键单击节点的一瞬间,菜单控件会被定位到实际的节点位置上并显示,当用户单击菜单项的时候会自动触发预设的菜单动作。菜单动作在head的javascript元素里设置
菜单触发动作的设置
注意到在用户自定义的菜单中有类似如下的注释
<!-- @operation(add_folder) -->
<td
这个注释最后是要被框架解析的,在客户端和服务器端都要被解析。它的意义为在这个注释下面的DOM模型讲将作为菜单动作的触发视图。这个例子中,当在它下面的td上单击菜单项时将会触发add_folder动作,框架会自动将当前treeModel的id也就是数据行的主键传递给触发动作的js方法作为参数。可以根据节点的category来为不同的节点设置不同的动作。在实际的项目编码中,每一个动作都要向web容器进行请求以实现一定的业务逻辑。
注意,有的HTML元素有一些隐式的子元素比如<tbody>,这样当在第一个<tr>上标注动作的时候实际上是标注在<tbody>上的。
2. <mx:treeIterator elementName="element">
用于迭代树状模型,并设置被迭代的模型节点在jsp context中的帮定名。这个标签有点类似于jstl的迭代标签。迭代的是按照树形视图从上到下显示顺序来进行的。
这个标签中还可以设置排序器和过滤器。
可以在这个标签的子标签内使用${ element }的el表达式或jstl标签来取得TreeModel的属性值。每一个”element”都是一个mx.web.treeView.modelLayer.TreeModel实例
3. <mx:treeMenu_operation>
在此设置对于每一个节点要触发哪些动作。可以尝试修改演示项目,比如在这个标签中通过jstl的<if>添加条件,以根据不同的节点类型来设置不同的动作。
4. <mx:treeItem_img> 不用我解释了吧
5. <mx:TreeItem_str>
注意使用css样式来规范显示效果,比如<span style="vertical-align:12%;">,可以将这个样式去掉以观察区别。
如果要显示的文本含有 “> <”等字符请进行替换,否则会出现错误。
所有的代码必须写在子标签之内,不要在子标签之间书写代码,否则可能会引起一些错误。
关于树形结构的内容其实还有很多。在这个框架的”原型”中,大量的代码用于处理其他操作,比如CRUD操作,特别是如何在树形的数据库结构中进行各种有效快速的查询,以及如何对树形数据库进行初始化。这些操作需要一定技巧,以后我会继续发表相关的文章。
即将推出的GA版将要改进的内容
1. 将会实现属性文件
2. 将一些不必要的配置项去掉
3. 如果有需要,实现更复杂的映射模型
4. 提供一个完善的对于树状结构的演示实例
5. 如果时间充裕,提供eclipse插件
此框架的未来展望
框架内使用的js脚本不能与IE之外的浏览器兼容,未来可能会进行一些兼容性的工作。
目前这个插件只是模拟”动态”效果,在速度较快的局域网内可以运行良好。但对于网站项目,数以千计的节点生成的HTML代码数据量很大,对于网络传输是一个不小的负担,会产生延迟。在未来的版本中可以只将有限的节点信息传送到客户端(比如只有”浅层”的树状节点),其他节点通过ajax动态传送并生成。
演示项目及webTreeView框架jar包