文档对象模型(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或DocumentFragment):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时时刻刻就在您的身边。使用这种被广泛部署的工具,绝对是您的明智之举。
Dethe Elza现在最喜爱的头衔是“首席疯狂科学家(Chief Mad Scientist)”。可以通过电子邮件delza@livingcode.org与他联系。他在http://livingcode.blogspot.com/上主要记录着关于Python和Mac OS X方面的blog。欢迎对本专栏提出意见和建议。
我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。
我原创,你原创,我们的内容世界才会更加精彩!
【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】
微信公众号
TechTarget
官方微博
TechTarget中国
作者
相关推荐
-
数字化转型:如何更好地利用API和微服务
API,即应用程序编程接口,它提供给应用程序、开发人员访问其它应用的能力,而又无需访问源码,无需理解内部工作机制细节;简单地说,API就是实现应用与应用连接的一种隐形的桥梁。
-
金融行业数字转型:利用API构建新IT基础
从制造业、物流业,银行业到零售业,各行各业的根基都因应用经济的兴起发生着深刻的变革。在互联网和智能手机普及化的推动下,这种现象变得司空见惯。到2021年 ,蓬勃发展的全球应用经济的预估总值将达到6.3万亿美元,相比2016年的1.3万亿美元,增长近5倍。
-
如何使用Azure API管理服务?
在云和微服务架构时代,API是数字化业务的通用语言。根据分析公司Forrester Research预测,仅在美国,API管理工具的支出将在未来5年内达到近30亿美元。
-
私有存储云如何构建?
如何构建自己的私有存储云呢?在这之前,我们要先退后一步,思考一下云计算到底意味着什么。