Dethe Elza ( [email protected] ), 高級技術架構師, Blast Radius
文檔物件模型(Document Object Model,DOM)是用於操縱XML 和HTML 資料的最常用工具之一,然而它的潛力卻很少被充分挖掘出來。透過利用DOM 的優勢,並使它更加易用,您將獲得一款應用於XML 應用程式(包括動態Web 應用程式)的強大工具。
本期文章介紹了一位客串的專欄作家,同時也是我的朋友和同事Dethe Elza。 Dethe 在利用XML 進行Web 應用程式開發方面經驗豐富,在此,我要感謝他對我在介紹使用DOM 和ECMAScript 進行XML 程式設計這一方面的幫助。請密切注意本專欄,以了解Dethe 的更多專欄文章。
—— David Mertz
DOM 是處理XML 和HTML 的標準API 之一。由於它佔用記憶體大、速度慢,冗長,所以經常受到人們的指責。儘管如此,對於許多應用程式來說,它仍然是最佳選擇,而且比XML 的另一個主要API —— SAX 無疑要簡單得多。 DOM 正逐漸出現在一些工具中,像是Web 瀏覽器、SVG 瀏覽器、OpenOffice,等等。
DOM 很好,因為它是一種標準,並且被廣泛地實現,同時也內建到其他標準中。作為標準,它對資料的處理與程式語言無關(這可能是優點,也可能是缺點,但至少使我們處理資料的方式變得一致)。 DOM 現在不僅內建於Web 瀏覽器,而且也成為許多基於XML 的規範的一部分。既然它已經成為您的工具的一部分,也許您偶爾還會使用它,我想現在應該充分利用它為我們帶來的功能了。
在使用DOM 一段時間後,您會看到形成了一些模式— 您想要重複做的事情。快捷方式可以幫助您處理冗長的DOM,並創建自解釋的、優雅的程式碼。這裡收集了一些我經常使用的技巧和訣竅,還有一些JavaScript 範例。
insertAfter 和prependChild
第一個訣竅就是「沒有訣竅」。 DOM 有兩種方法可以將孩子節點加入容器節點(通常是一個Element,也可能是一個Document 或Document Fragment):appendChild(node) 和insertBefore(node, referenceNode)。看起來似乎缺了什麼。假如我想在一個參考節點後面插入或由前新增(prepend)一個子節點(使新節點位於列表中的第一位),我該怎麼做呢?很多年以來,我的解決方法是編寫下列函數:
清單1. 插入和由前新增的錯誤方法
function insertAfter(parent, node, referenceNode) {
if(referenceNode.nextSibling) {
parent.insertBefore(node, referenceNode.nextSibling);
} else {
parent.appendChild(node);
}
}
function prependChild(parent, node) {
if (parent.firstChild) {
parent.insertBefore(node, parent.firstChild);
} else {
parent.appendChild(node);
}
}
實際上,就像清單1 一樣,insertBefore() 函數已經被定義為,在參考節點為空時會回到appendChild()。因此,您可以不使用上面的方法,而使用清單2 中的方法,或跳過它們僅使用內建函數:
清單2. 插入和由前新增的正確方法
function insertAfter(parent, node, referenceNode) {
parent.insertBefore(node, referenceNode.nextSibling);
}
function prependChild(parent, node) {
parent.insertBefore(node, parent.firstChild);
}
如果您剛剛接觸DOM 編程,有必要指出的是,雖然您可以使多個指標指向一個節點,但該節點只能存在於DOM 樹中的一個位置。因此,如果您想將它插入樹中,沒必要先將它從樹中移除,因為它會自動移除。當節點重新排序時,這種機制很方便,只需將節點插入到新位置即可。
根據此機制,若想交換兩個相鄰節點(稱為node1 和node2)的位置,可以使用下列方案之一:
node1.parentNode.insertBefore(node2, node1);
或
node1.parentNode.insertBefore(node1.nextSibling, node1);
還可以使用DOM 做什麼?
Web 頁面中大量應用了DOM。若造訪bookmarklets 網站(請參閱參考資料),您會發現很多有創意的簡短腳本,它們可以重新編排頁面,提取鏈接,隱藏圖片或Flash 廣告,等等。
但是,因為Internet Explorer 沒有定義Node 介面常數(可以用於識別節點類型),所以您必須確保在遺漏介面常數時,首先為Web 在DOM 腳本中定義介面常數。
清單3. 確保節點被定義
if (!window['Node']) {
window.Node = new Object();
Node.ELEMENT_NODE = 1;
Node.ATTRIBUTE_NODE = 2;
Node.TEXT_NODE = 3;
Node.CDATA_SECTION_NODE = 4;
Node.ENTITY_REFERENCE_NODE = 5;
Node.ENTITY_NODE = 6;
Node.PROCESSING_INSTRUCTION_NODE = 7;
Node.COMMENT_NODE = 8;
Node.DOCUMENT_NODE = 9;
Node.DOCUMENT_TYPE_NODE = 10;
Node.DOCUMENT_FRAGMENT_NODE = 11;
Node.NOTATION_NODE = 12;
}
清單4 展示如何擷取包含在節點中的所有文字節點:
清單4. 內部文本
function innerText(node) {
// is this a text or CDATA node?
if (node.nodeType == 3 || node.nodeType == 4) {
return node.data;
}
var i;
var returnValue = [];
for (i = 0; i < node.childNodes.length; i++) {
returnValue.push(innerText(node.childNodes[i]));
}
return returnValue.join('');
}
快捷方式
人們常常抱怨DOM 太冗長,簡單的功能也需要寫大量程式碼。例如,如果您想建立一個包含文字並回應點擊按鈕的<div> 元素,程式碼可能類似於:
清單5. 建立<div> 的“漫長之路”
function handle_button() {
var parent = document.getElementById('myContainer');
var div = document.createElement('div');
div.className = 'myDivCSSClass';
div.id = 'myDivId';
div.style.position = 'absolute';
div.style.left = '300px';
div.style.top = '200px';
var text = "This is the first text of the rest of this code";
var textNode = document.createTextNode(text);
div.appendChild(textNode);
parent.appendChild(div);
}
若經常按照這種方式建立節點,鍵入所有這些程式碼會使您很快就疲憊不堪。一定有更好的解決方案—— 確實有這樣的解決方案!下面這個實用工具可以幫助您建立元素、設定元素屬性和風格,並新增文字子節點。除了name 參數,其他參數都是可選的。
清單6. 函數elem() 捷徑
function elem(name, attrs, style, text) {
var e = document.createElement(name);
if (attrs) {
for (key in attrs) {
if (key == 'class') {
e.className = attrs[key];
} else if (key == 'id') {
e.id = attrs[key];
} else {
e.setAttribute(key, attrs[key]);
}
}
}
if (style) {
for (key in style) {
e.style[key] = style[key];
}
}
if (text) {
e.appendChild(document.createTextNode(text));
}
return e;
}
使用該捷徑,您能夠以更簡潔的方法建立清單5 中的<div> 元素。請注意,attrs 和style 參數是使用JavaScript 文字物件而給出的。
清單7. 建立<div> 的簡單方法
function handle_button() {
var parent = document.getElementById('myContainer');
parent.appendChild(elem('div',
{class: 'myDivCSSClass', id: 'myDivId'}
{position: 'absolute', left: '300px', top: '200px'},
'This is the first text of the rest of this code'));
}
在您想要快速建立大量複雜的DHTML 物件時,這種實用工具可以節省您大量的時間。模式在這裡是指,如果您有一種需要頻繁創建的特定的DOM 結構,請使用實用工具來創建它們。這不僅減少了您編寫的程式碼量,而且也減少了重複的剪下、貼上程式碼(錯誤的罪魁禍首),並且在閱讀程式碼時思路更加清晰。
接下來是什麼?
DOM 通常很難告訴您,按照文件的順序,下一個節點是什麼。以下有一些實用工具,可以幫助您在節點間前後移動:
清單8. nextNode 和prevNode
// return next node in document order
function nextNode(node) {
if (!node) return null;
if (node.firstChild){
return node.firstChild;
} else {
return nextWide(node);
}
}
// helper function for nextNode()
function nextWide(node) {
if (!node) return null;
if (node.nextSibling) {
return node.nextSibling;
} else {
return nextWide(node.parentNode);
}
}
// return previous node in document order
function prevNode(node) {
if (!node) return null;
if (node.previousSibling) {
return previousDeep(node.previousSibling);
}
return node.parentNode;
}
// helper function for prevNode()
function previousDeep(node) {
if (!node) return null;
while (node.childNodes.length) {
node = node.lastChild;
}
return node;
}
輕鬆使用DOM
有時候,您可能想要遍歷DOM,在每個節點上呼叫函數或從每個節點傳回一個值。實際上,由於這些想法非常具有普遍性,所以DOM Level 2 已經包含了一個稱為DOM Traversal and Range 的擴展(為迭代DOM 所有節點定義了物件和API),它用來為DOM 中的所有節點應用函數和在DOM 中選擇一個範圍。因為這些函數沒有在Internet Explorer 中定義(至少目前是這樣),所以您可以使用nextNode() 來做一些類似的事情。
在這裡,我們的想法是創建一些簡單、普通的工具,然後以不同的方式組裝它們來達到預期的效果。如果您很熟悉函數式編程,這看起來會很親切。 Beyond JS 庫(參考參考資料)將此概念發揚光大。
清單9. 函數式DOM 實用工具
// return an Array of all nodes, starting at startNode and
// continuing through the rest of the DOM tree
function listNodes(startNode) {
var list = new Array();
var node = startNode;
while(node) {
list.push(node);
node = nextNode(node);
}
return list;
}
// The same as listNodes(), but works backwards from startNode.
// Note that this is not the same as running listNodes() and
// reversing the list.
function listNodesReversed(startNode) {
var list = new Array();
var node = startNode;
while(node) {
list.push(node);
node = prevNode(node);
}
return list;
}
// apply func to each node in nodeList, return new list of results
function map(list, func) {
var result_list = new Array();
for (var i = 0; i < list.length; i++) {
result_list.push(func(list[i]));
}
return result_list;
}
// apply test to each node, return a new list of nodes for which
// test(node) returns true
function filter(list, test) {
var result_list = new Array();
for (var i = 0; i < list.length; i++) {
if (test(list[i])) result_list.push(list[i]);
}
return result_list;
}
清單9 包含了4 個基本工具。 listNodes() 和listNodesReversed() 函數可以擴展到一個可選的長度,這與Array 的slice() 方法效果類似,我把這個作為留給您的練習。另一個要注意的是,map() 和filter() 函數是完全通用的,用於處理任何列表(不只是節點列表)。現在,我向您展示它們的幾種組合方式。
清單10. 使用函數式實用工具
// A list of all the element names in document order
function isElement(node) {
return node.nodeType == Node.ELEMENT_NODE;
}
function nodeName(node) {
return node.nodeName;
}
var elementNames = map(filter(listNodes(document),isElement), nodeName);
// All the text from the document (ignores CDATA)
function isText(node) {
return node.nodeType == Node.TEXT_NODE;
}
function nodeValue(node) {
return node.nodeValue;
}
var allText = map(filter(listNodes(document), isText), nodeValue);
您可以使用這些實用工具來提取ID、修改樣式、找到某種節點並移除,等等。一旦DOM Traversal and Range API 被廣泛實現,您無需先建立列表,就可以用它們修改DOM 樹。它們不但功能強大,而且工作方式也與我在上面所強調的方式類似。
DOM 的危險地帶
注意,核心DOM API 並不能讓您將XML 資料解析到DOM,或將DOM 序列化為XML。這些功能都定義在DOM Level 3 的擴展部分“Load and Save”,但它們還沒有完全實現,因此現在不要考慮這些。每個平台(瀏覽器或其他專業DOM 應用程式)都有自己在DOM 和XML間轉換的方法,但跨平台轉換不在本文討論範圍之內。
DOM 並不是十分安全的工具- 特別是使用DOM API 建立不能作為XML 序列化的樹。絕對不要在同一個程式中混合使用DOM1 非名稱空間API 和DOM2 名稱空間感知的API(例如,createElement 和createElementNS)。如果您使用名稱空間,請盡量在根元素位置聲明所有名稱空間,並且不要覆蓋名稱空間前綴,否則情況會非常混亂。一般來說,只要按照慣例,就不會觸發使您陷入麻煩的臨界情況。
如果您一直使用Internet Explorer 的innerText 和innerHTML 進行解析,那麼您可以嘗試使用elem() 函數。透過建立類似的一些實用工具,您將獲得更多便利,並且繼承了跨平台程式碼的優越性。將這兩種方法混合使用是非常糟糕的。
某些Unicode 字元並沒有包含在XML 中。 DOM 的實作使您可以添加它們,但後果是無法序列化。這些字元包括大多數的控製字元和Unicode 代理程式對(surrogate pair)中的單一字元。只有您試圖在文件中包含二進位資料時才會遇到這種情況,但這是另一種轉向(gotcha)情況。
結束語
我已經介紹了DOM 能做的很多事情,但是DOM(和JavaScript)可以做的事情遠不止這些。仔細研究、揣摩這些例子,看看如何使用它們來解決可能需要客戶端腳本、範本或專用API 的問題。
DOM 有自己的限制和缺點,但同時也擁有眾多優點:它內建於許多應用程式中;無論使用Java 技術、Python 或JavaScript,它都以相同方式運作;它非常便於使用SAX;使用上述的模板,它使用起來既簡潔又強大。越來越多的應用程式開始支援DOM,這包括基於Mozilla的應用程式、OpenOffice 和Blast Radius 的XMetaL。越來越多的規範需要DOM,並對它加以擴展(例如,SVG),因此DOM 時時刻刻就在您的身邊。使用這種廣泛部署的工具,絕對是您的明智之舉。