分享
 
 
 

利用ASP和XML制作菜单导航系统

王朝asp·作者佚名  2006-12-16
窄屏简体版  字體: |||超大  

1.1 概述

高效地利用Web页面有限的空间并不容易,特别是要在页面中安排大量的链接时尤为困难。如何才能组织好各种链接以便为其它重要内容留出空间?是一次性地展示所有链接还是分成多个页面把它们深深地隐藏起来?显然,这两种方法都不理想。利用DHTML,我们可以在为用户提供快速方便的访问链接的同时,为其它内容保留足够的页面空间。

本文介绍一个菜单系统的实现。这个菜单与Windows的“开始”菜单非常相似,用户只需点击一次鼠标即可访问所有链接。菜单的内容由XML文档定义,客户端行为由DHTML实现。XML文档的解析在服务器端完成,因此对用户浏览器是否支持XML并没有要求。

本文接下来的内容具体介绍其实现,包括XML文档的结构以及如何分析这个XML文档、配合客户端JavaScript一起构造出最终的菜单。

1.2 静态和动态

页面内容动态生成是指在用户请求时才动态地(自动地)生成内容,静态内容则是将自动生成的内容保存为文件,用户请求的时候发送给他们的是这些静态的文件。有读者曾经来信指出这个问题,我们在《再论Web内容的发布——用XML与ASP分离内容与设计》中转发了读者来信并谈了自己的看法。

依赖于菜单的实际大小和菜单内容变化的频繁程度,有些时候可能不想让菜单动态生成。这时候可以将菜单代码做成静态的:运行本文介绍的脚本,将所生成的菜单HTML代码保存为包含文件再直接引用就可以了。如果可以采用这个方案的话,它将节省大量宝贵的服务器资源。

本文以动态菜单为例具体介绍实现原理。不过为了方便读者,在下载包中我们提供了两个版本的服务器脚本,分别用于动态生成菜单和生成静态的菜单HTML代码。使用静态版本时,先打开xmlCreateMenu_Entry.asp页面,在这里输入如下两个信息:第一是所使用的XML文件,第二是包含文件的路径,这个包含文件将保存HTML格式的菜单代码。输入上述信息后即可自动运行服务器脚本生成菜单代码。以后如果要改变菜单内容,只要修改XML文件,然后再重复上述过程就可以了。

使用静态菜单时,参考本文default.asp中的动态菜单使用方法,只需将default.asp中的< !--#include file="XMLCreateMenu_Dynamic.asp"-- >改成包含静态菜单文件即可。

1.3 数据结构

XML无疑是当前最为热门的话题之一,人们普遍相信未来的Web就在于XML。不过本文使用XML来定义菜单内容并非是为了赶时髦,在这里使用XML确实有其特别的好处。

首先,既然使用了菜单系统,那么它就不大可能只用在一、二个页面上,往往是整个网站的所有页面都要用到同一菜单。这就要求能够快速地构造菜单,尽量地减少访问菜单定义数据所消耗的时间。第二,菜单是一个层次结构,定义菜单内容的数据最好也具有层次结构。这不仅可以方便地体现出各个菜单项之间的从属关系(从而有利于修改),而且在本文后面我们还可以看到,用递归函数来分析层次结构的数据是相当方便的。

XML可以很好地满足这两个要求,不仅数据可以快速地访问(无需额外的网络连接,无需登录数据库等),而且XML能够非常自然地描述出数据的层次结构。此外,前面我们已经提到,XML文档的解析在服务器端完成,因此并不要求用户浏览器支持XML。

编辑XML文档既可以使用普通的文本编辑器,也有许多专用的XML编辑器可供选择,有关这方面的内容以及XML基础概念的说明,请参见《XML简明教程》,也请参见XML Tools。

创建菜单系统的第一步是在XML文档中定义菜单内容。在考虑了几种可能的方法之后,我们决定采用最简单的一种方法,使得不太熟悉XML的读者也能理解和更改这个XML文件。每一个菜单项至少包含两个数据,即菜单项的文本和链接;如果菜单项拥有多个子菜单,可以很方便地加入子节点。为保持整个系统的简洁性,菜单最多不能超过三层。下面是本文XML文件的一个片断。

< menuItem >

< hyperLink/ >

< name >娱乐休闲< /name >

< menuItem >

< hyperLink >Link1.asp< /hyperLink >

< name >游戏< /name >

< /menuItem >

< menuItem >

< hyperLink >link4.asp< /hyperLink >

< name >汽车/摩托车< /name >

< /menuItem >

< /menuItem >

每一个菜单项由一个< menuItem >标记定义,它至少有两个子节点:子节点< hyperLink >定义的是该菜单项的链接,而< name >包含该菜单项文本。

可以看到,前面代码中的第一个< hyperLink >没有给出链接,这是因为这个菜单项用来弹出子菜单,它并不指向任何具体的Web页面。子节点的定义方法和父节点定义方法相同,使用的都是< menuItem >标记。这种分层结构可以一直嵌套下去,但我们已经提到,嵌套层次不宜过多,菜单系统的唯一目的应该是方便用户访问,而不是使得访问更加困难。 2.1 遍历XML树

在XML文档中定义了菜单内容后,接下来的任务是遍历和分析这些层次结构的数据、生成菜单的HTML代码。这主要通过一个递归函数walkTree实现。这部分代码的效率非常重要,不能为了生成菜单而让用户等待太长的时间。

在调用递归函数walkTree之前,我们首先要找出XML文档的根。这个根可以利用以下代码得到(完整的代码请从本文后面下载):

var XMLRoot = XMLDoc.documentElement;

在得到文档的根后,接下来程序创建一个数组来描述菜单项之间的层次关系。如下面的代码所示,这个数组以JavaScript的eval函数动态命名。变量startMenu决定完整菜单名字的基础部分,从本文后面我们还可以看到,客户端代码也要利用该菜单名字来激活菜单。

var image = "< img align=right vspace=2 height=10

width=10 border=0 src='images/tri.gif' >";

var currentNode = "";

var newNode = "";

var currentMenu = "";

var startMenu = "menu1";

var level = 1;

var arrayHolderArray= new Array();

var arrayNamesArray = new Array();

var tempString= "";

//数组的名字为DIV名字加"_Array"

eval("var " + startMenu + "_Array = new Array()")

for (var i=0;i< XMLRoot.childNodes.length;i++) {

currentNode = XMLRoot.childNodes.item(i);

if (currentNode.hasChildNodes() == true && currentNode.childNodes.length >1) {

currentMenu = startMenu + "_" + (i+1);

thisMenu = startMenu;

if (currentNode.childNodes.length >2) {

sMenuItem = escape("< SPAN id="" + thisMenu + "_span" + (i+1) +

" class='cellOff'onMouseOver="stateChange('" +

currentMenu +"',this," + level +

")" onMouseOut="stateChange('',this,'')" >" +

image + currentNode.childNodes.item(1).text +

"< /SPAN >< BR >

");

eval(startMenu + "_Array[i] = sMenuItem")

walkTree(currentNode)

} else {

sMenuItem = escape("< SPAN id="" + thisMenu + "_span" + (i+1) +

"" class='cellOff' " +

"onMouseOver="stateChange('',this,'');hideDiv(" +

level + ")" onMouseOut="stateChange('',this,'')" " +

"onClick="location.href='" +

currentNode.childNodes.item(0).text + "'" >" +

currentNode.childNodes.item(1).text +

"< /SPAN >< BR >

");

eval(startMenu + "_Array[i] = sMenuItem")

}

}

}

创建初始的数组(名为menu1_Array)之后,程序检查第一个< menuItem >标记是否含有子节点。要获得子节点的数量非常简单,只需访问父节点的childNodes.length属性即可得到。

检查子节点数量的时候,判断条件是childNotes.length属性是否大于1,这是因为节点中的文本也是一个节点,length值包含它。如果以等于1作为判断条件,即使唯一的子节点是文本节点而不是元素节点,if语句也将返回true(尽管我们还可以检查子节点的类型,但不如当前方法简洁)。

分析节点的同时,所有必需的< DIV >属性和客户端事件响应代码(文本)都保存到了变量sMenuItem,然而又把sMenuItem保存到数组,它在数组中的位置由当前在循环中的位置决定。如果当前节点包含元素类型的子节点,则调用walkTree()并将当前节点作为参数传递给它。

2.2 递归函数walkTree

所谓的递归函数,就是自己调用自己的函数。递归函数非常适合于处理层次结构的数据。在遍历XML树时,使用递归函数可以减少大量的代码,一个简单的递归结构可以处理数量庞大的子菜单。

walkTree()函数以一个节点为参数,与前面所讨论的过程类似,它将创建数组并检查childNode.length属性。下面是walkTree()的完整代码:

function walkTree(node) {

level += 1;

// 数组名字为DIV的名字加"_Array"

eval("var " + currentMenu + "_Array = new Array()")

for (var j=2;j< node.childNodes.length;j++) {

newNode = node.childNodes.item(j)

if (newNode.hasChildNodes() == true && newNode.childNodes.length >2) {

// 每一个节点拥有0=链接和1=文本节点

// 因此如仅有这些子节点则不必再次调用函数

currentMenu = currentMenu + "_" + (j-1);

thisMenu = currentMenu.substring(0,currentMenu.length-2);

sMenuItem = escape("< SPAN id="" + thisMenu + "_span" + (j-1) +

"" class='cellOff' " + "onMouseOver="stateChange('" + currentMenu +

"',this," + level + ")" onMouseOut="stateChange('',this,'')" >" +

image + newNode.childNodes.item(1).text +

"< /SPAN >< BR >

");

eval(thisMenu + "_Array[j-2] = sMenuItem");

walkTree(newNode);

} else {

sMenuItem = escape("< SPAN id="" + currentMenu + "_span" +

(j-1) + "" class='cellOff' " +

"onMouseOver="stateChange('',this,'');hideDiv(" +

level + ")" onMouseOut="stateChange('',this,'')" " +

"onClick="location.href='" +

newNode.childNodes.item(0).text + "'" >" +

newNode.childNodes.item(1).text +

"< /SPAN >< BR >

");

eval(currentMenu + "_Array[j-2] = sMenuItem");

}

}

arrayNamesArray[arrayNamesArray.length] = currentMenu;

tempString = (unescape(eval(currentMenu + "_Array.join('')")))

arrayHolderArray[arrayHolderArray.length] = tempString

currentMenu = currentMenu.substring(0,currentMenu.length-2);

//结束函数返回前一菜单项

level -= 1;

}

如果能够找到子节点,则函数以当前节点为参数调用自己。每次函数调用自己都生成相应的数组和sMenuItem变量。当前菜单的名字通过变量currentMenu保存,并在thisMenu中保存一个新的名字,这使得程序可以方便地找出菜单项之间的层次关系,在后面的客户端脚本我们可以看到这一点。

请仔细阅读walkTree()函数的结束部分。在前面代码中创建的arrayNamesArray数组用来保存菜单的名字,接下来变量tempString获取当前节点的所有数组信息并将它组织成为一个字符串,这个字符串被加入到另外一个名为arrayHolderArray的数组。数组arrayNamesArray的用途是保存菜单项的名字,把菜单项(及其下属的子菜单项)放入< DIV >区时需要用到这些名字,所有< DIV >区将参考arrayNamesArray的内容命名。数组arrayHolderArray保存所有那些将写入各个< DIV >区的信息,这些信息包括菜单项的名字、链接以及客户端脚本的事件信息。

这些数组之间的关系初看起来有点复杂,仔细观察其实不难理解。walkTree函数利用这些数组在少量的代码之内完成了大量的工作。

准备好上述数组之后,接下来就可以把< DIV >区以及它们的内容写入Web页面。下面的代码负责这部分工作(参见XMLcreateMenu_Dynamic.asp)。

//翻转数组以便形成正确的< DIV >次序

arrayHolderArray.reverse()

arrayNamesArray.reverse()

//遍历数组,输出< DIV >标记以及各个菜单项的代码

for (i=0;i< arrayNamesArray.length;i++) {

Response.Write("< div id='" + arrayNamesArray[i] + "' class='clsMenu' >");

Response.Write(arrayHolderArray[i]);

Response.Write("< /div >

");

}

为何要将数组的次序翻转一下呢?简而言之,它将简化我们以后的操作。翻转之后,程序输出的< DIV >区即以层次结构中的最前面子菜单到最后面子菜单为序;或者说,翻转数组之后保证各个< DIV >区在Z-轴方向上有正确的次序。如果你还是不能一下子领会,那么请看下图,这是在注释了两个reverse方法调用后的菜单效果:

翻转数组之后,接着就输出< DIV >标记及其内容。如前所述,< DIV >区的名字由数组arrayNamesArray决定,< DIV >区的内容由数组arrayHolderArray决定。下面的代码是前面代码所输出< DIV >区的一个片断,其他< DIV >区也非常相似:

< div id='menu1' class='clsMenu' >

< span id="menu1_span1"

class='cellOff'

onMouseOver="stateChange('',this,'');hideDiv(1)"

onMouseOut="stateChange('',this,'')"

onClick="location.href='/default/default.asp'" >主页

< /span >< br >

< span id="menu1_span2"

class='cellOff'

onMouseOver="stateChange('menu1_2',this,1)"

onMouseOut="stateChange('',this,'')" >

< img align=right vspace=2 height=10

width=10 border=0 src='images/tri.gif' >娱乐休闲

< /span >< br >

< /div >

XMLMenuScript.js文件包含了所有支持客户端菜单操作的JavaScript代码。在文件前面定义的变量可以用来定制菜单项在各种状态(选中或不选中)下的显示颜色,具体请参见XMLMenuScript.js中的说明。应当说明的是,在当前实现中,客户端脚本代码要求运行在IE 4或以上版本。如果你希望菜单同时能够支持Netscape浏览器,这部分代码要作相应的修改。

当用户单击某个链接(如本文的“网站”)时菜单系统启动。启动菜单的HTML代码如下所示:

< A id="start" onClick="startIt('menu1',this,0)" >

< B >< FONT color="#FFFFFF" >网站< /FONT >< /B >

< IMG src="images/yellow_arrow_down2.gif" width="20" height="11" border=0 >

< /A >

onClick事件中指定的startIt()函数有三个参数:菜单名字,一个链接本身的引用,菜单层次。由于此时刚开始启动菜单系统,因此指定层次为0。

虽然一个页面中可以有一个以上的菜单,但任何时候可见的菜单只有一个。startIt()函数检查菜单是否已经激活。如果没有激活,则调用下面的stateChange()函数。此外,startIt()还必须在菜单活动的时候隐藏HTML < SELECT >元素和Java Applet(如果存在的话,参见startIt()的实现代码)。

function stateChange(menu,thisItem,level) {

//menu =待显示的菜单,thisItem=当前菜单项的< SPAN >

//level=菜单嵌套层次

//鼠标所指向的菜单项改变,改变高亮度状态

if (currentSpanElement != thisItem.id && started != true) {

//这行代码仅在第一次进入时有用

if (currentSpanElement == "") currentSpanElement = thisItem.id;

eItemOld = eval("document.all('" + currentSpanElement + "')");

eItemNew = eval("document.all('" + thisItem.id + "')");

eParent = eItemNew.parentElement;

//必须设置DIV背景色,否则默认透明

eParent.style.background = offCellColor;

//取消以前鼠标停留菜单项的高亮度颜色

eItemOld.style.background = offCellColor;

eItemOld.style.color = offTextColor;

//突出显示新选中的菜单项

eItemNew.style.background = onCellColor;

eItemNew.style.color = onTextColor;

//跟踪最后鼠标停留的位置

currentSpanElement = thisItem.id;

}

if (menu != "") {

eMenu = eval("document.all('" + menu + "')");

eItem = eval("document.all('" + thisItem.id + "')");

hideDiv(level);

//跟踪已经打开(显示)的菜单

menuArray[menuArray.length] = menu;

var positionX =eItem.parentElement.offsetLeft +

offsetMenuX + document.body.scrollLeft;

var positionY = eItem.parentElement.offsetTop +

eItem.offsetTop + offsetMenuY + document.body.scrollTop;

if (started) {

positionX = clickX + startDistanceX + document.body.scrollLeft

positionY = clickY + startDistanceY + document.body.scrollTop

}

//如果屏幕宽度不足,将菜单显示位置左移

if ((positionX + eMenu.offsetWidth)>= document.body.clientWidth) {

positionX -= (eMenu.offsetWidth * 1.3);

positionY += 15;

}

//如果菜单位置偏左,则右移

if ((positionX + eMenu.offsetWidth) < = eMenu.offsetWidth) {

positionX += (eMenu.offsetWidth * 1.3);

}

//如果菜单偏下,则上移菜单,使其底部和浏览窗口底部对齐

if ((positionY + eMenu.offsetHeight)>= document.body.clientHeight) {

if (started != true) positionY = document.body.clientHeight - eMenu.offsetHeight;

}

eMenu.style.left = positionX;

eMenu.style.top = positionY;

//如果没有在ASP脚本中翻转数组,使用下面这行代码

//eMenu.style.zIndex = level;

eMenu.style.visibility='visible';

}

//菜单已经显示

started = false;

}

stateChange()函数有三个参数:菜单名字,当前菜单项的< SPAN >元素,以及层次。如果用户选择了一个不同的< SPAN >元素(菜单项),stateChange()改变该菜单项的背景和文本颜色,同时改变以前选中菜单项的状态。此外,stateChange()还负责调用其他函数隐藏菜单以及在菜单位置不合适时,修改菜单的位置。

菜单所用的样式在menuStyle.css中,修改该文件可调整菜单的外观。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有