ドキュメント オブジェクト モデル (DOM) は、XML および HTML データを操作するために最も一般的に使用されるツールの 1 つですが、その可能性が完全に活用されることはほとんどありません。 DOM を利用して使いやすくすることで、動的 Web アプリケーションを含む XML アプリケーション用の強力なツールが得られます。
この号には、友人で同僚のコラムニスト、デテ・エルザがゲストとして登場します。 Dethe は XML を使用した Web アプリケーションの開発に豊富な経験があり、DOM と ECMAScript を使用した XML プログラミングを私に紹介してくれたことに感謝します。 Dethe の詳細については、このコラムをご覧ください。
——David Mertz
DOM は、XML と HTML を処理するための標準 API の 1 つです。多くの場合、メモリを大量に消費し、処理が遅く、冗長であると批判されます。それでも、これは多くのアプリケーションにとって最良の選択であり、XML の他の主要な API である SAX よりもはるかに単純であることは確かです。 DOM は、Web ブラウザ、SVG ブラウザ、OpenOffice などのツールに徐々に登場しています。
DOM は標準であり、広く実装され、他の標準に組み込まれているため、優れています。標準として、そのデータの処理はプログラミング言語に依存しません (これが強みになる場合もそうでない場合もありますが、少なくともデータの処理方法に一貫性が生まれます)。 DOM は現在、Web ブラウザーに組み込まれているだけでなく、多くの XML ベースの仕様の一部でもあります。今ではそれがあなたの武器の一部となり、おそらく今でも時々使用しているので、それが私たちに与えてくれるものを最大限に活用する時期が来たと思います。
しばらく DOM を操作すると、何度も繰り返したくなるパターンが開発されることがわかります。ショートカットは、長い DOM を操作し、一目瞭然で洗練されたコードを作成するのに役立ちます。ここでは、私が頻繁に使用するヒントとテクニックのコレクションと、いくつかの JavaScript の例を示します。
insertAfter と PRependChild
最初のトリックは、「トリックはありません」です。 DOM には、子ノードをコンテナ ノード (通常は要素ですが、ドキュメントまたはドキュメント フラグメント) に追加するための 2 つのメソッドがあります。appendChild(node) と insertBefore(node,referenceNode) です。何かが足りないようです。参照ノードの後に子ノードを挿入または先頭に追加したい場合はどうすればよいですか (新しいノードをリストの最初にします)。長年にわたり、私の解決策は次の関数を書くことでした:
リスト 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() 関数は次のように定義されています。 () 参照ノードが空の場合。したがって、上記のメソッドを使用する代わりに、リスト 2 のメソッドを使用することも、それらをスキップして組み込み関数だけを使用することもできます。
リスト 2. 前の
function insertAfter(parent, node, referenceNode) { parent.insertBefore(node, referenceNode.nextSibling); } function prependChild(parent, node) { parent.insertBefore(node, parent.firstChild); }
による挿入と追加の正しい方法DOM プログラミングに取り組む場合は、ノードを指す複数のポインターを持つことができますが、そのノードは DOM ツリー内の 1 つの場所にしか存在できないことに注意することが重要です。したがって、ツリーに挿入する場合は、自動的に削除されるため、最初にツリーから削除する必要はありません。このメカニズムは、ノードを新しい位置に挿入するだけでノードの順序を変更する場合に便利です。
このメカニズムによれば、2 つの隣接するノード (node1 と node2 と呼ばれる) の位置を交換したい場合は、次のいずれかの解決策を使用できます。
node1 .parentNode.insertBefore(node1.nextSibling, node1);
DOM で他に何ができるでしょうか?
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 は、ノードに含まれるすべてのテキスト ノードを抽出する方法を示しています。不満 DOM は冗長すぎて、単純な機能には多くのコードが必要です。たとえば、テキストを含み、ボタンのクリックに応答する
リスト 5.
若频繁按照这种方式创建节点,键入所有这些代码会使您很快疲惫不堪。必须有更好的解决方案 —— 确实有这样的解决方案!下面这个实用工具可以帮助您创建元素、设置元素属性和风格,并添加文本子节点。除了 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 中的
清单 7. 创建
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 を使用するのは非常に簡単です。 、シンプルかつ強力な使い方ができます。 Mozilla ベースのアプリケーション、OpenOffice、Blast Radius の XMetaL など、DOM をサポートし始めているアプリケーションが増えています。 DOM (SVG など) を必要とする仕様や拡張仕様が増えているため、DOM は常に身近にあります。この広く導入されているツールを使用するのは賢明な選択です。
上記は、XML Question: Beyond DOM (DOM を簡単に使用するためのヒントとコツ) の内容です。その他の関連コンテンツについては、PHP 中国語 Web サイト (www.php.cn) に注目してください。