IBM®
直接進入主要内容
    Taiwan  [選擇 ]      使用條款
 
 
    
     首頁      產品      服務與解决方案      技術支援與下載      個人專區     
直接進入主要内容

developerWorks 台灣  >  XML  >

超越 DOM

輕鬆使用 DOM 的技巧和訣竅

developerWorks
文件選項

需要JavaScript的文件選項無法顯示


級別: 中級

Dethe Elza, 高級技術架構師

2005 年 6 月 16 日

文件物件模型(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,也可能是一個 DocumentDocumentFragment):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 樹中的一個位置。因此,如果您想將它插入到樹中,沒必要先將它從樹中移除,因為它會自動被移除。當重新將節點排序時,這種機制很方便,僅需將節點插入到新位置即可。

根據這種機制,若想交換兩個相鄰節點(稱為 node1node2)的位置,可以使用下列方案之一:

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> 元素。注意,attrsstyle 參數是使用 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() 函數可以擴展到一個可選的長度,這與 Arrayslice() 方法效果類似,我把這個作為留給您的練習。另一個需要注意的是,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(例如,createElementcreateElementNS)。如果您使用命名空間,請儘量在根元素位置宣告所有命名空間,並且不要覆蓋命名空間的前置詞(prefixes),否則情況會非常混亂。一般來說,只要按照慣例,就不會觸發使您陷入麻煩的臨界情況。

如果您一直使用 Internet Explorer 的 innerTextinnerHTML 進行解析,那麼您可以試試使用 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 時時刻刻就在您的身邊。使用這種被廣泛部署的工具,絕對是您的明智之舉。



回到頂端


參考資料

  • 您可以參閱本文在 developerWorks 全球網站上的 英文原文

  • 下載 JavaScript 程式庫,它包含了上面的腳本和一個用於測試這些腳本的簡單 測試頁面

  • 直接瀏覽 DOM 發源地 —— W3C 的 DOM 資源頁面 ,其中包含到所有與文件物件模型相關的標準的連結。

  • 查看 Jesse Ruderman 的 bookmarklets。雖然 Ruderman 沒有創造術語“bookmarkets”,但他收集了很多一流的、簡短的、書簽似的 JavaScript,使用它們開發 DOM 的巨大潛力,使您的瀏覽器可以為您帶來更多幫助。

  • 瀏覽 Sjoerd Visscher 的 Beyond JS 程式庫,它提供了遠遠超過我在這裡提及的用於函數式程式設計的工具。如果您可以將事物抽像為函數,那麼 JavaScript 將會成為您得心應手的工具。

  • DOM API 的標準參考在 W3C。這裡是 DOM2 到 JavaScript (ECMAScript)映射 的網址。

  • 瞭解 AJAX 為什麼已經引起了這麼大的迴響。它使用了非同步呼叫來使伺服器即時升級 Web 應用程式。您可以使用上述的許多技術,並閱讀 非同步通信工具

  • 瞭解一下 XML 編輯器和工具的 XMetaL 系列,它們都支援 DOM API。它們由作者所在的公司 Blast Radius 開發。

  • 在 developerWorks 的 Developer Bookstore 瞭解更多 XMl 相關的書籍,其中包括 David Mertz 的 Text Processing in Python 一書。

  • 瞭解如何才能成為 IBM 認證的 XML 及相關技術的開發人員


回到頂端


關於作者

Dethe Elza 現在最喜愛的頭銜是“首席瘋狂科學家(Chief Mad Scientist)”。可以透過電子郵件 delza@livingcode.org 與他聯繫。他在 http://livingcode.blogspot.com/ 上主要記錄著關於 Python 和 Mac OS X 方面的 blog。歡迎對本專欄提出意見和建議。




回到頂端


對本文的評價

不甚滿意!(1)
可再加強 (2)
持平 (3)
相當不錯 (4)
受益匪淺!(5)



回到頂端



    關於IBM隱私權條款聯絡我們