18.1 浏览器对XML DOM 的支持

优质
小牛编辑
115浏览
2023-12-01
在正式的规范诞生以前,浏览器提供商实现的XML 解决方案不仅对XML 的支持程度参差不齐,而且对同一特性的支持也各不相同。DOM2 级是第一个提到动态创建XML DOM 概念的规范。DOM3级进一步增强了XML DOM,新增了解析和序列化等特性。然而,当DOM3 级规范的各项条款尘埃落定之后,大多数浏览器也都实现了各自不同的解决方案。

18.1.1 DOM2 级核心

我们在第12 章曾经提到过,DOM2级在document.implementation 中引入了createDocument()方法。IE9+、Firefox、Opera、Chrome 和Safari 都支持这个方法。想一想,或许你还记得可以在支持DOM2级的浏览器中使用以下语法来创建一个空白的XML 文档:
var xmldom = document.implementation.createDocument(namespaceUri, root, doctype);
在通过JavaScript 处理XML 时,通常只使用参数root,因为这个参数指定的是XML DOM 文档元素的标签名。而namespaceUri 参数则很少用到,原因是在JavaScrip 中管理命名空间比较困难。最后,doctype 参数用得就更少了。 因此,要想创建一个新的、文档元素为<root>的XML 文档,可以使用如下代码:
var xmldom = document.implementation.createDocument("", "root", null);
alert(xmldom.documentElement.tagName); //"root"
var child = xmldom.createElement("child");
xmldom.documentElement.appendChild(child);
运行一下 这个例子创建了一个XML DOM文档,没有默认的命名空间,也没有文档类型。但要注意的是,尽管不需要指定命名空间和文档类型,也必须传入相应的参数。具体来说,给命名空间URI 传入一个空字符串,就意味着未指定命名空间,而给文档类型传入null,就意味着不指定文档类型。变量xmldom中保存着一个DOM2 级Document 类型的实例,带有第12 章讨论过的所有DOM方法和属性。我们这个例子显示了文档元素的标签名,然后又创建并给文档元素添加了一个新的子元素。 要检测浏览器是否支持DOM2 级XML,可以使用下面这行代码:
var hasXmlDom = document.implementation.hasFeature("XML", "2.0");
在实际开发中,很少需要从头开始创建一个XML 文档,然后再使用DOM 文档为其添加元素。更常见的情况往往是将某个XML 文档解析为DOM 结构,或者反之。由于DOM2 级规范没有提供这种功能,因此就出现了一些事实标准。

18.1.2 DOMParser类型

为了将XML 解析为DOM文档,Firefox 引入了DOMParser 类型;后来,IE9、Safari、Chrome 和Opera 也支持了这个类型。在解析XML 之前,首先必须创建一个DOMParser 的实例,然后再调用parseFromString()方法。这个方法接受两个参数:要解析的XML 字符串和内容类型(内容类型始终都应该是"text/xml")。返回的值是一个Document 的实例。来看下面的例子。
var parser = new DOMParser();
var xmldom = parser.parseFromString("<root><child/></root>", "text/xml");
alert(xmldom.documentElement.tagName); //"root"
alert(xmldom.documentElement.firstChild.tagName); //"child"
var anotherChild = xmldom.createElement("child");
xmldom.documentElement.appendChild(anotherChild);
var children = xmldom.getElementsByTagName("child");
alert(children.length); //2
运行一下 在这个例子中,我们把一个简单的XML 字符串解析成了一个DOM文档。解析得到的DOM 结构以<root>作为其文档元素,该元素还有一个<child>子元素。此后,就可以使用DOM 方法对返回的这个文档进行操作了。 DOMParser 只能解析格式良好的XML,因而不能把HTML 解析为HTML 文档。在发生解析错误时, 仍然会从parseFromString() 中返回一个Document 对象, 但这个对象的文档元素是<parsererror>,而文档元素的内容是对解析错误的描述。下面是一个例子。
<parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">XML
Parsing Error: no element found Location: file:///I:/My%20Writing/My%20Books/
Professional%20JavaScript/Second%20Edition/Examples/Ch15/DOMParserExample2.htm Line
Number 1, Column 7: <sourcetext> & lt;root & gt; ------^</sourcetext > < /parsererror>
Firefox 和Opera 都会返回这种格式的文档。Safari 和Chrome 返回的文档也包含<parsererror>元素,但该元素会出现在发生解析错误的地方。IE9 会在调用parseFromString()的地方抛出一个解析错误。 由于存在这些差别,因此确定是否发生解析错误的最佳方式就是,使用一个try-catch 语句块,如果没有错误,则通过getElementsByTagName()来查找文档中是否存在<parsererror>元素,如下面的例子所示。
var parser = new DOMParser(),
xmldom,
errors;
try {
xmldom = parser.parseFromString("<root>", "text/xml");
errors = xmldom.getElementsByTagName("parsererror");
if (errors.length > 0) {throw new Error("Parsing error!");
}
} catch(ex) {
alert("Parsing error!");
}
运行一下 这例子显示,要解析的字符串中缺少了闭标签</root>,而这会导致解析错误。在IE9+中,此时会抛出错误。在Firefox 和Opera 中,文档元素将是<parsererror>,而在Safari 和Chrome 中,<parsererror>是<root>的第一个子元素。调用getElementsByTagName("parsererror")能够应对这两种情况。如果这个方法返回了元素,就说明有错误发生,继而通过一个警告框显示出来。当然,你还可以更进一步,从错误元素中提取出错误信息。

18.1.3 XMLSerializer类型

在引入DOMParser 的同时,Firefox 还引入了XMLSerializer 类型,提供了相反的功能:将DOM文档序列化为XML 字符串。后来,IE9+、Opera、Chrome 和Safari 都支持了XMLSerializer。 要序列化DOM 文档,首先必须创建XMLSerializer 的实例,然后将文档传入其serializeTo-String ()方法,如下面的例子所示。
var serializer = new XMLSerializer();
var xml = serializer.serializeToString(xmldom);
alert(xml);
运行一下 但是,serializeToString()方法返回的字符串并不适合打印,因此看起来会显得乱糟糟的。XMLSerializer 可以序列化任何有效的DOM 对象,不仅包括个别的节点,也包括HTML 文档。 将HTML 文档传入serializeToString()以后,HTML 文档将被视为XML 文档,因此得到的代码也将是格式良好的。
如果将非DOM 对象传入serializeToString(),会导致错误发生。

18.1.4 IE8 及之前版本中的XML

事实上,IE 是第一个原生支持XML 的浏览器,而这一支持是通过ActiveX 对象实现的。为了便于桌面应用程序开发人员处理XML,微软创建了MSXML 库;但微软并没有针对JavaScript 创建不同的对象,而只是让Web 开发人员能够通过浏览器访问相同的对象。 第8 章曾经介绍过ActiveXObject 类型,通过这个类型可以在JavaScript 中创建ActiveX 对象的实例。同样,要创建一个XML 文档的实例,也要使用ActiveXObject 构造函数并为其传入一个表示XML 文档版本的字符串。有6 种不同的XML 文档版本可以供选择。
  • Microsoft.XmlDom:最初随同IE 发布;不建议使用。
  • MSXML2.DOMDocument:为方便脚本处理而更新的版本,建议仅在特殊情况下作为后备版本使用。
  • MSXML2.DOMDocument.3.0:为了在JavaScript 中使用,这是最低的建议版本。
  • MSXML2.DOMDocument.4.0:在通过脚本处理时并不可靠,使用这个版本可能导致安全警告。
  • MSXML2.DOMDocument.5.0:在通过脚本处理时并不可靠,使用这个版本同样可能导致安全警告。
  • MSXML2.DOMDocument.6.0:通过脚本能够可靠处理的最新版本。
在这6 个版本中,微软只推荐使用MSXML2.DOMDocument.6.0 或MSXML2.DOMDocument.3.0; 前者是最新最可靠的版本,而后者则是大多数Windows 操作系统都支持的版本。可以作为后备版本的MSXML2.DOMDocument,仅在针对IE5.5 之前的浏览器开发时才有必要使用。 通过尝试创建每个版本的实例并观察是否有错误发生,可以确定哪个版本可用。例如:
function createDocument() {
if (typeof arguments.callee.activeXString != "string") {var versions = ["MSXML2.DOMDocument.6.0", "MSXML2.DOMDocument.3.0", "MSXML2.DOMDocument"],i,len;for (i = 0, len = versions.length; i < len; i++) {try {new ActiveXObject(versions[i]);arguments.callee.activeXString = versions[i];break;} catch(ex) {//跳过}}
}
return new ActiveXObject(arguments.callee.activeXString);
}
运行一下 这个函数中使用for 循环迭代了每个可能的ActiveX 版本。如果版本无效, 则创建新ActiveXObject 的调用就会抛出错误;此时,catch 语句会捕获错误,循环继续。如果没有发生错误,则可用的版本将被保存在这个函数的activeXString 属性中。这样,就不必在每次调用这个函数时都重复检查可用版本了——直接创建并返回对象即可。 要解析XML 字符串,首先必须创建一个DOM 文档,然后调用loadXML()方法。新创建的XML文档完全是一个空文档,因而不能对其执行任何操作。为loadXML()方法传入的XML 字符串经解析之后会被填充到DOM 文档中。来看下面的例子。
var xmldom = createDocument();
xmldom.loadXML("<root><child/></root>");
alert(xmldom.documentElement.tagName); //"root"
alert(xmldom.documentElement.firstChild.tagName); //"child"
var anotherChild = xmldom.createElement("child");
xmldom.documentElement.appendChild(anotherChild);
var children = xmldom.getElementsByTagName("child");
alert(children.length); //2
运行一下 在新DOM 文档中填充了XML 内容之后,就可以像操作其他DOM 文档一样操作它了(可以使用任何方法和属性)。 如果解析过程中出错,可以在parseError 属性中找到错误消息。这个属性本身是一个包含多个属性的对象,每个属性都保存着有关解析错误的某一方面信息。
  • errorCode:错误类型的数值编码;在没有发生错误时值为0。
  • filePos:文件中导致错误发生的位置。
  • line:发生错误的行。
  • linepos:发生错误的行中的字符。
  • reason:对错误的文本解释。
  • srcText:导致错误的代码。
  • url:导致错误的文件的URL(如果有这个文件的话)。
另外,parseError 的valueOf()方法返回errorCode 的值,因此可以通过下列代码检测是否发生了解析错误。
if (xmldom.parseError != 0){
    alert("Parsing error occurred.");
}
错误类型的数值编码可能是正值,也可能是负值,因此我们只需检测它是不是等于0。要取得有关解析错误的详细信息也很容易,而且可以将这些信息组合起来给出更有价值的解释。来看下面的例子。
if (xmldom.parseError != 0){
alert("An error occurred:\nError Code: "  + xmldom.parseError.errorCode + "\n"  + "Line: " + xmldom.parseError.line + "\n"  + "Line Pos: " + xmldom.parseError.linepos + "\n"  + "Reason: " + xmldom.parseError.reason);
}
运行一下 应该在调用loadXML()之后、查询XML 文档之前,检查是否发生了解析错误。

1. 序列化XML

IE 将序列化XML 的能力内置在了DOM文档中。每个DOM 节点都有一个xml 属性,其中保存着表示该节点的XML 字符串。例如:
alert(xmldom.xml);
文档中的每个节点都支持这个简单的序列化机制,无论是序列化整个文档还是某个子文档树,都非常方便。

2. 加载XML 文件

IE 中的XML 文档对象也可以加载来自服务器的文件。与DOM3 级中的功能类似,要加载的XML文档必须与页面中运行的JavaScript 代码来自同一台服务器。同样与DOM3 级规范类似,加载文档的方式也可以分为同步和异步两种。要指定加载文档的方式,可以设置async 属性,true 表示异步,false表示同步(默认值为true)。来看下面的例子。
var xmldom = createDocument();
xmldom.async = false;
在确定了加载XML 文档的方式后,调用load()可以启动下载过程。这个方法接受一个参数,即要加载的XML 文件的URL。在同步方式下,调用load()后可以立即检测解析错误并执行相关的XML处理,例如:
var xmldom = createDocument();
xmldom.async = false;
xmldom.load("example.xml");
if (xmldom.parseError != 0) {
//处理错误
} else {
alert(xmldom.documentElement.tagName); //"root"
alert(xmldom.documentElement.firstChild.tagName); //"child"
var anotherChild = xmldom.createElement("child");
xmldom.documentElement.appendChild(anotherChild);
var children = xmldom.getElementsByTagName("child");
alert(children.length); //2
alert(xmldom.xml);
}
运行一下 由于是以同步方式处理XML 文件,因此在解析完成之前,代码不会继续执行,这样的编程工作要简单一点。虽然同步方式比较方便,但如果下载时间太长,会导致程序反应很慢。因此,在加载XML文档时,通常都使用异步方式。 在异步加载XML 文件的情况下,需要为XML DOM文档的onreadystatechange 事件指定处理程序。有4 个就绪状态(ready state)。
  • 1:DOM正在加载数据。
  • 2:DOM已经加载完数据。
  • 3:DOM已经可以使用,但某些部分可能还无法访问。
  • 4:DOM已经完全可以使用。
在实际开发中,要关注的只有一个就绪状态:4。这个状态表示XML 文件已经全部加载完毕,而且已经全部解析为DOM 文档。通过XML 文档的readyState 属性可以取得其就绪状态。以异步方式加载XML 文件的典型模式如下。
var xmldom = createDocument();
xmldom.async = true;
xmldom.onreadystatechange = function() {
if (xmldom.readyState == 4) {if (xmldom.parseError != 0) {alert("An error occurred:\nError Code: " + xmldom.parseError.errorCode + "\n" + "Line: " + xmldom.parseError.line + "\n" + "Line Pos: " + xmldom.parseError.linepos + "\n" + "Reason: " + xmldom.parseError.reason);} else {alert(xmldom.documentElement.tagName); //"root"alert(xmldom.documentElement.firstChild.tagName); //"child"var anotherChild = xmldom.createElement("child");xmldom.documentElement.appendChild(anotherChild);var children = xmldom.getElementsByTagName("child");alert(children.length); //2alert(xmldom.xml);}
}
};
xmldom.load("example.xml");
运行一下 要注意的是,为onreadystatechange 事件指定处理程序的语句,必须放在调用load()方法的语句之前;这样,才能确保在就绪状态变化时调用该事件处理程序。另外,在事件处理程序内部,还必须注意要使用XML 文档变量的名称(xmldom),不能使用this 对象。原因是ActiveX 控件为预防安全问题不允许使用this 对象。当文档的就绪状态变化为4 时,就可以放心地检测是否发生了解析错误,并在未发生错误的情况下处理XML 了。
虽然可以通过XML DOM文档对象加载XML文件,但公认的还是使用XMLHttp-Request 对象比较好。有关XMLHttpRequest 对象及Ajax 的相关内容,将在第21章讨论。

18.1.5 跨浏览器处理XML

很少有开发人员能够有福气专门针对一款浏览器做开发。因此,编写能够跨浏览器处理XML 的函数就成为了常见的需求。对解析XML 而言,下面这个函数可以在所有四种主要浏览器中使用。
function parseXml(xml) {
var xmldom = null;
if (typeof DOMParser != "undefined") {xmldom = (new DOMParser()).parseFromString(xml, "text/xml");var errors = xmldom.getElementsByTagName("parsererror");if (errors.length) {throw new Error("XML parsing error:" + errors[0].textContent);}
} else if (typeof ActiveXObject != "undefined") {xmldom = createDocument();xmldom.loadXML(xml);if (xmldom.parseError != 0) {throw new Error("XML parsing error: " + xmldom.parseError.reason);}
} else {throw new Error("No XML parser available.");
}
return xmldom;
}
运行一下

这个parseXml()函数只接收一个参数,即可解析的XML 字符串。在函数内部,我们通过能力检测来确定要使用的XML 解析方式。DOMParser 类型是受支持最多的解决方案,因此首先检测该类型是否有效。如果是,则创建一个新的DOMParser 对象,并将解析XML 字符串的结果保存在变量xmldom中。由于DOMParser 对象在发生解析错误时不抛出错误(除IE9+之外),因此还要检测返回的文档以确定解析过程是否顺利。如果发现了解析错误,则根据错误消息抛出一个错误。

函数的最后一部分代码检测了对ActiveX 的支持,并使用前面定义的createDocument()函数来创建适当版本的XML 文档。与使用DOMParser 时一样,这里也需要检测结果,以防有错误发生。如果确实有错误发生,同样也需要抛出一个包含错误原因的错误。如果上述XML 解析器都不可用,函数就会抛出一个错误,表示无法解析了。

在使用这个函数解析XML 字符串时,应该将它放在try-catch 语句当中,以防发生错误。来看下面的例子。

var xmldom = null;
try {
xmldom = parseXml("<root><child/></root>");
} catch(ex) {
alert(ex.message);
}
//进一步处理
运行一下 对序列化XML 而言,也可以按照同样的方式编写一个能够在四大浏览器中运行的函数。例如:
function serializeXml(xmldom) {
if (typeof XMLSerializer != "undefined") {return (new XMLSerializer()).serializeToString(xmldom);
} else if (typeof xmldom.xml != "undefined") {return xmldom.xml;
} else {throw new Error("Could not serialize XML DOM.");
}
}
运行一下 这个serializeXml()函数接收一个参数,即要序列化的XML DOM 文档。与parseXml()函数一样,这个函数首先也是检测受到最广泛支持的特性,即XMLSerializer。如果这个类型有效,则使用它来生成并返回文档的XML 字符串。由于ActiveX 方案比较简单,只使用了一个xml 属性,因此这个函数直接检测了该属性。如果上述两方面尝试都失败了,函数就会抛出一个错误,说明序列化不能进行。一般来说,只要针对浏览器使用了适当的XML DOM 对象,就不会出现无法序列化的情况,因而也就没有必要在try-catch 语句中调用serializeXml()。结果,就只需如下一行代码即可:
var xml = serializeXml(xmldom);
只不过由于序列化过程的差异,相同的DOM 对象在不同的浏览器下,有可能会得到不同的XML字符串。