近日作项目,项目中有一个树,它的节点竟然有1700多个,为了能快速的显示它,或者说让用户感觉挺快的显示。我和同事们尝试了几种不同的方法。
原来的树,是基于老外写的一个js做的,将节点信息,转为js数组,在通过页面内定义的js树节点对象一一加入。以前最动时,曾用过800个节点,勉强可以接受,本以为在这个项目中,也有可能使用,所以试了一下,结果,惨不忍睹,足足花了3-4秒,树才出来@^@#^@#^@^#。
接着,采用同事的方法,使用了一个自己写的js代码,将树分散为几个个数组片断,分段加载,总的来说,速度还行。可是,分段过程需要人工干预...而且分段加载会出现某些节点后台加载偏慢地问题,另外,如果节点都被加载后,页面操作也有点慢。当需要实时更新,如树节点维护模块,这个方法就不是很可行。
又尝试另一个同事的JSP TreeTag,发现也不算很快(也许是没正确配置的原因罢)。
最后,实在没办法,只好自己编写了一段程序,我把它称为不完全展开树方式。
一般使用树的模块,往往操作集中在几个地方。根节点、兄弟节点、子节点(也许还有父节点),那么从这个角度考虑的话,可以有选择性的显示这些节点,而其他节点不需要输出到Html中。
因此,我制作了一个树的实现页面,以树的当前节点ID为参数,输出实际的树,从服务器端读取(当然结合AJAX技术,也可以用js展开),点击节点前面的图标,则当前节点就切换过去。
根据这个办法,如果,只显示根节点、节点所在路径的所有节点,节点的子节点,节点的兄弟节点,一般查询的次数会比较少,而且大多是单节点查找。
在目前的使用中情况较好.截图如下:

另外配合,这个树,通过ajax技术,可以支持快速节点定位(通过ID)和一次返回所有子节点ID。
目前运行状况良好,我已经改写了所有相关的树,采用此模式处理。
附核心逻辑代码:(工具函数Common.jsp主要定义数据源和一些处理数据的基本方法)
<%@ page contentType="text/html; charset=gb2312"%>
<%@ include file="Common.jsp" %>
<%!
public static String rootParent="0"; //根节点的查找方法
public static int nodeType=Text_TYPE; //节点主键的类型
//国标分类树
public static String treeSQL="select GBID,GParentID,GBName from pclassandgb where 1=1 ";
public static String nodeCountSQL="select Count(GBID) from pclassandgb where 1=1 ";
public static String keyField="GBID";
public static String nameField="GBName";
public static String parentField="GParentID";
public static String spaceStr=" ";
//递归获得节点的完整路径
String getFullPathEx(String nodeKey) throws Exception {
String fullpath="";
//获取当前的节点信息
String nodeParent; //当前节点父ID
javax.sql.RowSet nodeRs=null;
nodeRs = openRowSet(treeSQL+" and "+keyField+"="+toSQL(nodeKey,nodeType));
if (nodeRs.next()){
nodeParent=checkNull(nodeRs.getString(2));
if (nodeParent.equals(rootParent)){
fullpath=nodeKey;
}else{
fullpath=getFullPathEx(nodeParent)+"-"+nodeKey;
}
}else{
fullpath="";
}
return fullpath;
}
//子节点个数
int getChildren(String nodeKey) throws Exception {
int children=0;
if(bDebug) System.err.println(nodeCountSQL+" and "+ parentField + "=" + toSQL(nodeKey,nodeType));
children=Integer.parseInt(checkNull(getString(nodeCountSQL+" and "+ parentField + "=" + toSQL(nodeKey,nodeType))));
return children;
}
//节点深度
int getNodeDepth(String fullPath) throws Exception {
int nodeDepth=0;
String[] nodeKey = ("-"+fullPath+"-").split("-");
for (int j = 1; j <= nodeKey.length; j++) {
if (nodeKey[j - 1].equalsIgnoreCase("")) {
}
else {
nodeDepth++;
if(bDebug) System.err.println(nodeKey[j - 1]);
}
}
return nodeDepth;
}
//获得节点所在路径上的所有节点ID列表
String[] getParentNodes(String fullPath) throws Exception {
//获得完整父节点序列
int nodeNumber=getNodeDepth(fullPath);
//定义父节点数组
String[] parentNodes=new String[nodeNumber];
int nodeDepth=0;
String[] nodeKey = ("-"+fullPath+"-").split("-");
//给父节点数组赋值
for (int j = 1; j <= nodeKey.length; j++) {
if (nodeKey[j - 1].equalsIgnoreCase("")) {
}
else {
nodeDepth++;
parentNodes[nodeDepth-1]=nodeKey[j - 1];
}
}
return parentNodes;
}
//输出节点信息到Html中
String WriteNode(String nodeKey) throws Exception{
//计算深度 ,转换为空格
//查询名称出来,并输出
String nodeMsg="";
String nodeParent; //当前节点父ID
String nodeName;
int nodeDepth=getNodeDepth(getFullPathEx(nodeKey));
javax.sql.RowSet nodeRs=null;
nodeRs = openRowSet(treeSQL+" and "+keyField+"="+toSQL(nodeKey,nodeType));
if (nodeRs.next()){
String spaceMsg="";
for (int i=0;i<nodeDepth-1;i++){
spaceMsg=spaceMsg+spaceStr;
}
//计算,如果有子节点则 显示一种图片,否则另一种图片
String imgMsg="";
if (getChildren(nodeKey)>0){
imgMsg="<img border=0 src=/"images/left/closed.gif/"></img>";
}else{
imgMsg="<img border=0 src=/"images/left/document.gif/"></img>";
}
nodeMsg=nodeMsg+
"<Div class='NDS0'>"+
"<Div class='NDS'>"+spaceMsg+
"<a target='_self' href='SelectGBMakeTree.jsp?no="+
checkNull(nodeRs.getString(1))+
"'>"+
imgMsg+
"</a>"+
"<span href='SelectGBMakeTree.jsp?no="+
checkNull(nodeRs.getString(1))+
"' onclick='javascript:eval(retChild(/""+checkNull(nodeRs.getString(1)) +"/"));' >"+
checkNull(nodeRs.getString(3))+
"</span></div></div>";
}else{
nodeMsg="";
}
return nodeMsg;
}
%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gbk">
<title>搜索条件录入</title>
<link type="text/css" rel="stylesheet" href="/query/csslib/cecp.css">
<script src="/query/jslib/urlencode.js"></script>
<script src="/query/jslib/xmlrpc.js"></script>
<script type="text/javascript">
var done=true;
//注册服务
XMLRPC.onerror = function(e){
alert("执行远程调用时,发生错误:"+e.name+"/r/n详细信息:"+e.message);
return true;
}
try{
RPCHOST='<%=getHost(request)%>';
AllFunc = XMLRPC.getService(RPCHOST+"rpc/rpc4pclassandgb.jsp");
}catch(e){
alert("连接远程服务提供方时错误,发生错误:"+e.name+"/r/n详细信息:"+e.message);
}
AllFunc.add("AllFunc.retChild", "retChild");
function retChild(nodeKey){
done=true;
result="";
result = AllFunc.retChild(escape(nodeKey));
if (done) {
return unescape(result);
}else{
return "";
}
}
</script>
</head>
<body>
<div id="treeBox" class="TR" >
<%
String[] parentNodes=null; //父节点列表
String currentNodeKey=getParam(request,"no"); //当前节点
if (currentNodeKey.equals("")){
}else{
parentNodes=getParentNodes(getFullPathEx(currentNodeKey)); //获得父节点列表
}
//获得根节点信息
javax.sql.RowSet nodeListRs=null;
nodeListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(rootParent,nodeType));
boolean EOF=!nodeListRs.next();
while (!EOF){
//绘制所有根节点
out.println(WriteNode(nodeListRs.getString(1))+"");
if (currentNodeKey.equals("")){
//不绘制子节点了
}else{
//从根节点开始
if(checkNull(nodeListRs.getString(1)).equals(parentNodes[0])){
//绘制父节点序列,
//System.err.println(parentNodes.length==1);
for (int j=0 ;j<parentNodes.length;j++){
if (parentNodes.length==1){
//System.err.println("测试"+(parentNodes.length==1));
if (!(j==0)) out.println(WriteNode(parentNodes[j])+"");
//只绘制子节点序列
javax.sql.RowSet childListRs=null;
childListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(currentNodeKey,nodeType));
boolean CEOF=!childListRs.next();
while (!CEOF){
out.println(WriteNode(childListRs.getString(1))+"");
CEOF=!childListRs.next();
}
}else{
//System.err.println(parentNodes.length==1);
//不是根节点时,才绘制兄弟节点
//当绘制到当前节点时
if(j==(parentNodes.length-1)){
javax.sql.RowSet brotherListRs=null;
brotherListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(parentNodes[parentNodes.length-2],nodeType));
boolean BEOF=!brotherListRs.next();
while (!BEOF){
out.println(WriteNode(checkNull(brotherListRs.getString(1)))+"");
if ((brotherListRs.getString(1)).equals(currentNodeKey)){
//绘制子节点序列
javax.sql.RowSet childListRs=null;
childListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(currentNodeKey,nodeType));
boolean CEOF=!childListRs.next();
while (!CEOF){
out.println(WriteNode(childListRs.getString(1))+"");
CEOF=!childListRs.next();
}
}
BEOF=!brotherListRs.next();
}
}else{
if (!(j==0)) out.println(WriteNode(parentNodes[j])+"");
}
}
}
}else{
//不在其他节点展开
}
}
EOF=!nodeListRs.next();
}
%>
</div>
</body>
</html>
原来的树,是基于老外写的一个js做的,将节点信息,转为js数组,在通过页面内定义的js树节点对象一一加入。以前最动时,曾用过800个节点,勉强可以接受,本以为在这个项目中,也有可能使用,所以试了一下,结果,惨不忍睹,足足花了3-4秒,树才出来@^@#^@#^@^#。
接着,采用同事的方法,使用了一个自己写的js代码,将树分散为几个个数组片断,分段加载,总的来说,速度还行。可是,分段过程需要人工干预...而且分段加载会出现某些节点后台加载偏慢地问题,另外,如果节点都被加载后,页面操作也有点慢。当需要实时更新,如树节点维护模块,这个方法就不是很可行。
又尝试另一个同事的JSP TreeTag,发现也不算很快(也许是没正确配置的原因罢)。
最后,实在没办法,只好自己编写了一段程序,我把它称为不完全展开树方式。
一般使用树的模块,往往操作集中在几个地方。根节点、兄弟节点、子节点(也许还有父节点),那么从这个角度考虑的话,可以有选择性的显示这些节点,而其他节点不需要输出到Html中。
因此,我制作了一个树的实现页面,以树的当前节点ID为参数,输出实际的树,从服务器端读取(当然结合AJAX技术,也可以用js展开),点击节点前面的图标,则当前节点就切换过去。
根据这个办法,如果,只显示根节点、节点所在路径的所有节点,节点的子节点,节点的兄弟节点,一般查询的次数会比较少,而且大多是单节点查找。
在目前的使用中情况较好.截图如下:
目前运行状况良好,我已经改写了所有相关的树,采用此模式处理。
附核心逻辑代码:(工具函数Common.jsp主要定义数据源和一些处理数据的基本方法)
<%@ page contentType="text/html; charset=gb2312"%>
<%@ include file="Common.jsp" %>
<%!
public static String rootParent="0"; //根节点的查找方法
public static int nodeType=Text_TYPE; //节点主键的类型
//国标分类树
public static String treeSQL="select GBID,GParentID,GBName from pclassandgb where 1=1 ";
public static String nodeCountSQL="select Count(GBID) from pclassandgb where 1=1 ";
public static String keyField="GBID";
public static String nameField="GBName";
public static String parentField="GParentID";
public static String spaceStr=" ";
//递归获得节点的完整路径
String getFullPathEx(String nodeKey) throws Exception {
String fullpath="";
//获取当前的节点信息
String nodeParent; //当前节点父ID
javax.sql.RowSet nodeRs=null;
nodeRs = openRowSet(treeSQL+" and "+keyField+"="+toSQL(nodeKey,nodeType));
if (nodeRs.next()){
nodeParent=checkNull(nodeRs.getString(2));
if (nodeParent.equals(rootParent)){
fullpath=nodeKey;
}else{
fullpath=getFullPathEx(nodeParent)+"-"+nodeKey;
}
}else{
fullpath="";
}
return fullpath;
}
//子节点个数
int getChildren(String nodeKey) throws Exception {
int children=0;
if(bDebug) System.err.println(nodeCountSQL+" and "+ parentField + "=" + toSQL(nodeKey,nodeType));
children=Integer.parseInt(checkNull(getString(nodeCountSQL+" and "+ parentField + "=" + toSQL(nodeKey,nodeType))));
return children;
}
//节点深度
int getNodeDepth(String fullPath) throws Exception {
int nodeDepth=0;
String[] nodeKey = ("-"+fullPath+"-").split("-");
for (int j = 1; j <= nodeKey.length; j++) {
if (nodeKey[j - 1].equalsIgnoreCase("")) {
}
else {
nodeDepth++;
if(bDebug) System.err.println(nodeKey[j - 1]);
}
}
return nodeDepth;
}
//获得节点所在路径上的所有节点ID列表
String[] getParentNodes(String fullPath) throws Exception {
//获得完整父节点序列
int nodeNumber=getNodeDepth(fullPath);
//定义父节点数组
String[] parentNodes=new String[nodeNumber];
int nodeDepth=0;
String[] nodeKey = ("-"+fullPath+"-").split("-");
//给父节点数组赋值
for (int j = 1; j <= nodeKey.length; j++) {
if (nodeKey[j - 1].equalsIgnoreCase("")) {
}
else {
nodeDepth++;
parentNodes[nodeDepth-1]=nodeKey[j - 1];
}
}
return parentNodes;
}
//输出节点信息到Html中
String WriteNode(String nodeKey) throws Exception{
//计算深度 ,转换为空格
//查询名称出来,并输出
String nodeMsg="";
String nodeParent; //当前节点父ID
String nodeName;
int nodeDepth=getNodeDepth(getFullPathEx(nodeKey));
javax.sql.RowSet nodeRs=null;
nodeRs = openRowSet(treeSQL+" and "+keyField+"="+toSQL(nodeKey,nodeType));
if (nodeRs.next()){
String spaceMsg="";
for (int i=0;i<nodeDepth-1;i++){
spaceMsg=spaceMsg+spaceStr;
}
//计算,如果有子节点则 显示一种图片,否则另一种图片
String imgMsg="";
if (getChildren(nodeKey)>0){
imgMsg="<img border=0 src=/"images/left/closed.gif/"></img>";
}else{
imgMsg="<img border=0 src=/"images/left/document.gif/"></img>";
}
nodeMsg=nodeMsg+
"<Div class='NDS0'>"+
"<Div class='NDS'>"+spaceMsg+
"<a target='_self' href='SelectGBMakeTree.jsp?no="+
checkNull(nodeRs.getString(1))+
"'>"+
imgMsg+
"</a>"+
"<span href='SelectGBMakeTree.jsp?no="+
checkNull(nodeRs.getString(1))+
"' onclick='javascript:eval(retChild(/""+checkNull(nodeRs.getString(1)) +"/"));' >"+
checkNull(nodeRs.getString(3))+
"</span></div></div>";
}else{
nodeMsg="";
}
return nodeMsg;
}
%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gbk">
<title>搜索条件录入</title>
<link type="text/css" rel="stylesheet" href="/query/csslib/cecp.css">
<script src="/query/jslib/urlencode.js"></script>
<script src="/query/jslib/xmlrpc.js"></script>
<script type="text/javascript">
var done=true;
//注册服务
XMLRPC.onerror = function(e){
alert("执行远程调用时,发生错误:"+e.name+"/r/n详细信息:"+e.message);
return true;
}
try{
RPCHOST='<%=getHost(request)%>';
AllFunc = XMLRPC.getService(RPCHOST+"rpc/rpc4pclassandgb.jsp");
}catch(e){
alert("连接远程服务提供方时错误,发生错误:"+e.name+"/r/n详细信息:"+e.message);
}
AllFunc.add("AllFunc.retChild", "retChild");
function retChild(nodeKey){
done=true;
result="";
result = AllFunc.retChild(escape(nodeKey));
if (done) {
return unescape(result);
}else{
return "";
}
}
</script>
</head>
<body>
<div id="treeBox" class="TR" >
<%
String[] parentNodes=null; //父节点列表
String currentNodeKey=getParam(request,"no"); //当前节点
if (currentNodeKey.equals("")){
}else{
parentNodes=getParentNodes(getFullPathEx(currentNodeKey)); //获得父节点列表
}
//获得根节点信息
javax.sql.RowSet nodeListRs=null;
nodeListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(rootParent,nodeType));
boolean EOF=!nodeListRs.next();
while (!EOF){
//绘制所有根节点
out.println(WriteNode(nodeListRs.getString(1))+"");
if (currentNodeKey.equals("")){
//不绘制子节点了
}else{
//从根节点开始
if(checkNull(nodeListRs.getString(1)).equals(parentNodes[0])){
//绘制父节点序列,
//System.err.println(parentNodes.length==1);
for (int j=0 ;j<parentNodes.length;j++){
if (parentNodes.length==1){
//System.err.println("测试"+(parentNodes.length==1));
if (!(j==0)) out.println(WriteNode(parentNodes[j])+"");
//只绘制子节点序列
javax.sql.RowSet childListRs=null;
childListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(currentNodeKey,nodeType));
boolean CEOF=!childListRs.next();
while (!CEOF){
out.println(WriteNode(childListRs.getString(1))+"");
CEOF=!childListRs.next();
}
}else{
//System.err.println(parentNodes.length==1);
//不是根节点时,才绘制兄弟节点
//当绘制到当前节点时
if(j==(parentNodes.length-1)){
javax.sql.RowSet brotherListRs=null;
brotherListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(parentNodes[parentNodes.length-2],nodeType));
boolean BEOF=!brotherListRs.next();
while (!BEOF){
out.println(WriteNode(checkNull(brotherListRs.getString(1)))+"");
if ((brotherListRs.getString(1)).equals(currentNodeKey)){
//绘制子节点序列
javax.sql.RowSet childListRs=null;
childListRs = openRowSet(treeSQL+" and "+ parentField + "=" + toSQL(currentNodeKey,nodeType));
boolean CEOF=!childListRs.next();
while (!CEOF){
out.println(WriteNode(childListRs.getString(1))+"");
CEOF=!childListRs.next();
}
}
BEOF=!brotherListRs.next();
}
}else{
if (!(j==0)) out.println(WriteNode(parentNodes[j])+"");
}
}
}
}else{
//不在其他节点展开
}
}
EOF=!nodeListRs.next();
}
%>
</div>
</body>
</html>