首頁 >後端開發 >XML/RSS教程 >XML 問題: 超越DOM(輕鬆使用 DOM 的技巧與訣竅)

XML 問題: 超越DOM(輕鬆使用 DOM 的技巧與訣竅)

黄舟
黄舟原創
2017-02-27 16:30:061456瀏覽

文檔物件模型(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(&#39;&#39;);
}

 

    捷徑

    人們常抱怨DOM 太過冗長,簡單的功能也需要寫大量程式碼。例如,如果您想要建立一個包含文字並回應點擊按鈕的dc6dce4a544fdca2df29d5ac0ea9906b 元素,程式碼可能類似於:

清單5. 建立dc6dce4a544fdca2df29d5ac0ea9906b 的「漫長之路」

#
function handle_button() {
    var parent = document.getElementById(&#39;myContainer&#39;);
    var div = document.createElement(&#39;div&#39;);
    div.className = &#39;myDivCSSClass&#39;;
    div.id = &#39;myDivId&#39;;
    div.style.position = &#39;absolute&#39;;
    div.style.left = &#39;300px&#39;;
    div.style.top = &#39;200px&#39;;
    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 == &#39;class&#39;) {
                e.className = attrs[key];
            } else if (key == &#39;id&#39;) {
                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 中的 dc6dce4a544fdca2df29d5ac0ea9906b 元素。注意,attrs 和 style 参数是使用 Javascript 文本对象而给出的。

清单 7. 创建 dc6dce4a544fdca2df29d5ac0ea9906b 的简便方法

function handle_button() {
    var parent = document.getElementById(&#39;myContainer&#39;);
    parent.appendChild(elem(&#39;div&#39;,
      {class: &#39;myDivCSSClass&#39;, id: &#39;myDivId&#39;}
      {position: &#39;absolute&#39;, left: &#39;300px&#39;, top: &#39;200px&#39;},
      &#39;This is the first text of the rest of this code&#39;));
}

    在您想要快速创建大量复杂的 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 時時刻刻就在您的身邊。使用這種廣泛部署的工具,絕對是您的明智之舉。

 以上就是XML 問題: 超越DOM(輕鬆使用 DOM 的技巧與訣竅)的內容,更多相關內容請關注PHP中文網(www.php.cn)!


#
陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn