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=/School/UploadFiles_7810/201104/20110414130201849.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=/School/UploadFiles_7810/201104/20110414130201849.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="/School/UploadFiles_7810/201104/20110414130201265.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中,修改該文件可調整菜單的外觀。