XML 是 Web 的 SGML,但是它还没有像 XML 社区那样在 Web 上引人注目。XML 在 Web 上最突出的成就 —— XHTML —— 已经被政治和委员会设计所纠缠,并且其他雄心壮志、技术良好的规范 —— 例如 XForms 和 SVG —— 一直受到低使用率的困扰。有时候 XML 会在意想不到的方面在 Web 上获得成功,包括 XML 格式的 Web 提要(例如 RSS 类型和 Atom)的流行。
和其他 Web 上的技术一样,Web 上的 XML 以浏览器为中心,但是大部分关于在 Web 上处理 XML 的讨论都集中在服务器端。在 developerWorks 的 Firefox and XML 系列(参见 参考资料)中,我介绍了几种在 Firefox 浏览器中使用 XML 的方法。遗憾的是,跨浏览器处理 XML 甚至比跨浏览器处理 HTML 更加奇怪,这就是为什么这么多 Web 上的 XML 处理坚持相对安全的服务器端领域的部分原因。
许多动态 HTML 开发人员厌烦了跨浏览器的痛苦和浏览器之间脚本编写的怪癖 。几种出色的 JavaScript 库的出现使开发人员的工作更加轻松。这些库中最流行一种就是 jQuery,developerWorks 上有几篇文章已经对它进行了介绍。如果您知道如何绕开这些巨大的陷阱,您还可以使用 jQuery 来处理 XML。在本文中,我将展示如何在实际场景中联合使用 jQuery 和 XML,如何使用 Atom Web 提要,介绍一种在 jQuery 中处理 XML 的实用模式,并解决不幸遇到的实际问题。您需要对 XML、XML 名称空间、HTML、JavaScript 和 jQuery 库有基本的了解(请参阅 参考资料 了解更多介绍 jQuery 的文章)。
我将首先介绍最严重的问题。jQuery 并不能完全解决 XML 名称空间问题。这个众所周知的问题由来已久,人们尝试了各种解决方案,但结果都不太令人满意。理想的解决方案可能是利用 jQuery 支持 CSS Level 3 名称空间选择器(仍然是一个 W3C 工作草案,请参阅 参考资料),它将添加一个新的选择器,如下所示:
@namespace ex url(http://example.com); ex|quote { font-weight: bold }
第一行是 http://example.com 名称空间的前缀声明,第二行是一种使用新的名称空间组件的类型选择器,其中用竖线符号分隔已声明的前缀和本地名称。不幸的是,jQuery 并不支持这种方法,因此人们采取了各种方法来处理名称空间问题。
一种最常见的黑客方法是在 jQuery 中处理 XML 和名称空间时忽略名称空间,并选择完整的 qname(前缀和本地部分)。
$(xml).find("x\\:quote").each(function() { //process each node });
该代码通过 jQuery 的节点名称概念选择,即 DOM nodeName 属性。它包含一个冒号,是 jQuery 选择器保留的符号,并且必须使用反斜杠进行转义。反斜杠是 JavaScript 脚本保留的符号并且必须是一对。这种黑客方法在使用不同前缀的名称空间等效文档中无法使用。
据说有人成功地使用过以下方法的变体,即在伪属性 nodeName 上使用 jQuery 属性过滤器:
$(xml).find("[nodeName=x:quote]").each(function() { //process each node });
使用 jQuery 1.3.x 之前的版本,您需要在 nodeName 前面加上 @。但是,这样做与上一节 伪装前缀的重要性 中提到的方法有着相同的基本问题。它将破坏许多真实的名称空间场景。我尝试了以下变体,这种方法更合理:
$(xml).find("[namespaceURI='http://example.com'][localName='quote']") .each(function() { //process each node });
可惜这样不起作用。
这种混乱不完全是 jQuery 的错。DOM 为寻找节点提供了有效的方法:getElementsByTagName 和 getElementsByTagNameNS。后者旨在感知名称空间,接受名称空间的 URI 并忽略前缀,但遗憾的是,尽管其他浏览器都支持它,但 Microsoft® Internet Explorer® 除外。然而,jQuery 的目的是处理此类浏览器问题,以便消除人们的此类烦恼。一种可能的、牵强的理由是,jQuery 很大程度上以 CSS 作为其选择器的基础,并且即使是 W3C CSS Level 3 名称空间选择器也无法使它通过工作草案阶段。jQuery bug #155,“Get Namespaced Elements in XML Documents”(参见 参考资料),涵盖了这些问题,但是问题在 3 年之内没有得到解决。
Ryan Kelly 遇到此问题并做了一次大胆的尝试,为 XML Namespace Selector 创建了一个 jQuery 插件 jquery.xmlns.js(参见 参考资料)。它试图支持以下代码。
$.xmlns["ex"] = "http://example.com"; $(doc).find("ex|quote").each(...);
第一行是对该插件的全局名称空间声明 — 由于底层 jQuery 机制的局限性。它的确用典型的 jQuery 用语为名称空间范围提供一个非全局块。 遗憾的是,我在使用这种扩展时成败参半。我希望它能够改变,并最终找到合适的方法进入 jQuery 。
我最终选择的解决方案是创建一个简单插件,它不使用 jQuery 选择器做任何特殊工作,而是添加一个新的过滤器。您可以直接传递一个名称空间和本地名称到该过滤器,从而使结果集与节点匹配。请您按以下方法使用它:
$(xml).find('*').ns_filter('http://example.com', 'quote').each(function(){ .each(function() { //process each node });
ns_filter 是我写的特殊过滤器。执行一个单独的 find('*') 的需求看起来可能不优雅,更简单的变化可能是:
$(xml).find('quote').ns_filter('http://example.com').each(function(){ .each(function() { //process each node });
然而,这样做并不可行,因为您不能相信 jQuery 能够以名称空间中立(即作为本地名称选择器)的方式来处理查询,例如 find('quote')。我的过滤器实现将在下一节提供,作为安装 jQuery 来处理 XML 的一般系统的一部分。我在 Mac OS X Snow Leopard 操作系统下的 Firefox 3.5.5 和 Safari 4.0.4 ,以及 Windows® XP 操作系统最新的 Internet Explore 7 和 Internet Explorer 8 浏览器中对它进行了测试。
名称空间问题只是以下事实的症状:说到底,jQuery 是一个 HTML 工具。我发现,使用 jQuery 处理 XML 最实用的方式就是为 XML 文档创建一个 HTML 工作台,通过可靠地跨浏览器方法引用脚本,然后建立需要的暂时性解决方案,例如针对 XML 名称空间问题的解决方案。您可以用工作台模式准备并测试您基于浏览器的 XML 处理的模式和技术,您甚至还可以把工作台作为基于浏览器的应用程序本身的基础。
清单 1 (quotes.html)是 HTML 使用工作台的简单例子。它能够动态地从 XML 文件加载引用。
<html> <head> <title>jQuery XML workbench</title> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="workbench.js"></script> <script type="text/javascript" src="quotes.js"></script> <!-- Put the XML file or URL in the href attribute below: --> <link href="quotes1.xml" type="application/xml" rel="target_XML" /> </head> <body> <h1>A few quotations for your enjoyment</h1> <div id="update-target"><ol></ol></div> </body> </html>
您需要 script 元素来自动加载 jQuery、工作台 JavaScript 以及您的应用程序特定脚本。您还需要一个 link 元素来确定 target_XML 使用的 XML 文件。如果您需要使用多个 XML 文件,扩展工作台设置非常容易。清单 2(workbench.js)是工作台脚本。
/* workbench.js */ // The jQuery hook invoked once the DOM is fully ready $(document).ready(function(){ // Get the target XML file contents (Ajax call) var fileurl = $("link[rel='target_XML']").attr('href'); $.ajax({ url: fileurl, type: "GET", dataType: "xml", complete: xml_ready, error: error_func }); }); // Callback for when the Ajax call results in an error function error_func(result) { alert(result.responseText); } // ns_filter, a jQuery extension for XML namespace queries. (function($) { $.fn.ns_filter = function(namespaceURI, localName) { return $(this).filter(function() { var domnode = $(this)[0]; return (domnode.namespaceURI == namespaceURI && domnode.localName == localName); }); }; })(jQuery);
已经对工作台代码做了很好的注释,但这里还有一些需要注意的地方。名称空间过滤器是清单中最后一个函数。第一个函数是在主页 DOM 完全准备好之后调用一般的 jQuery hook。它为目标 XML 检索 URL,并调用 Ajax 来加载文件。注意,dataType: "xml" 命令 Ajax 机制来分析 XML 响应文件。如果出现错误,它将调用 error_func 回调函数,否则它将调用 xml_ready 回调函数,这是用户为实现应用程序行为而提供的。该回调函数使用了结果架构,您可以用responseXML 属性从中取回 XML 。清单 3(quotes.js)是这种情况下的应用程序代码。
/* quotes.js */ function xml_ready(result){ var xml = result.responseXML; //Make sure the target area for inserting data is clear $("#update-target ol").empty(); $(xml).find('*').ns_filter('http://example.com', 'q').each(function(){ var quote_text = $(this).text() $('<li></li>') .html(quote_text) .appendTo('#update-target ol'); }); //close each( }
清单 4 (quotes1.xml)是带有引用清单的 XML 文件。
<?xml version="1.0" encoding="utf-8"?> <x:quotes xmlns:x='http://example.com'> <x:q>Words have meaning and names have power</x:q> <x:q>Sticks and stones will break my bones, but names will never hurt me.</x:q> <x:q>The beginning of wisdom is to call things by their right names.</x:q> <x:q>Better to see the face than to hear the name. </x:q> </x:quotes>
请注意,我使用了 x 前缀,这意味着,理论上我可以尝试上文提到过的基于前缀的黑客方法。但是,如果我这样做,它将会被破坏,如果我用清单 5(quotes2.xml)中的 quotes 文件代替,它是与 清单 4 完全相当的名称空间,以及相同的 Canonical XML(参见 参考资料)。
<?xml version="1.0" encoding="utf-8"?> <quotes xmlns='http://example.com'> <q>Words have meaning and names have power</q> <q>Sticks and stones will break my bones, but names will never hurt me.</q> <q>The beginning of wisdom is to call things by their right names.</q> <q>Better to see the face than to hear the name. </q> </quotes>
如果您替代 清单 1 中的 quotes2.xml,您将发现它还起作用,这是一个针对名称空间的重要测试。图 1 是 quotes.html 的浏览器显示。
一旦您开始在 jQuery 中进行 XML 处理,您就能够处理更多有用的 XML 格式,包括 Web 提要格式,例如 RSS 和 Atom。在此部分我将使用 jQuery XML 工作台来显示来自一个 Web 页面上 Atom 提要的最新条目。清单 6 是 HTML 页面。
<html> <head> <title>jQuery XML workbench</title> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="workbench.js"></script> <script type="text/javascript" src="home.js"></script> <!-- Put the XML file or URL in the href attribute below: --> <link href="atom1.xml" type="application/xml" rel="target_XML" /> </head> <body> <h1>Caesar's home page</h1> <p>GALLIA est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur. Hi omnes lingua, institutis, legibus inter se differunt. </p> <p>Gallos ab Aquitanis Garumna flumen, a Belgis Matrona et Sequana dividit. </p> <p>Horum omnium fortissimi sunt Belgae, propterea quod a cultu atque humanitate provinciae longissime absunt, minimeque ad eos mercatores saepe commeant atque ea quae ad effeminandos animos pertinent important, proximique sunt Germanis, qui trans Rhenum incolunt, quibuscum continenter bellum gerunt. Qua de causa Helvetii quoque reliquos Gallos virtute praecedunt, quod fere cotidianis proeliis cum Germanis contendunt, cum aut suis finibus eos prohibent aut ipsi in eorum finibus bellum gerunt.</p> <h2>My <a href="feed.xml">Web feed</a></h2> <div id="update-target"></div> </body> </html>
清单 7(atom1.xml)是引用的 Atom 文件。
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en" xml:base="http://www.example.org"> <id>http://www.example.org/myfeed</id> <title>My Simple Feed</title> <updated>2005-07-15T12:00:00Z</updated> <link href="/blog" /> <link rel="self" href="/myfeed" /> <author><name>Uche Ogbuji</name></author> <entry> <id>http://www.example.org/entries/1</id> <title>A simple blog entry</title> <link href="/blog/2005/07/1" /> <updated>2005-07-14T12:00:00Z</updated> <summary>This is a simple blog entry</summary> </entry> <entry> <id>http://www.example.org/entries/2</id> <title /> <link href="/blog/2005/07/2" /> <updated>2005-07-15T12:00:00Z</updated> <summary>This is simple blog entry without a title</summary> </entry> </feed>
清单 8 是 home.js,包含了加载到工作台上的动态应用程序代码。
/* home.js */ var ATOM_NS = 'http://www.w3.org/2005/Atom'; function xml_ready(result){ var xml = result.responseXML; //Make sure the target area for inserting data is clear $("#update-target").empty(); $(xml).find('*').ns_filter(ATOM_NS, 'entry').each(function(){ var title_elem = $(this).find('*').ns_filter(ATOM_NS, 'title').clone(); var link_text = $(this).find('[rel="alternate"]') .ns_filter(ATOM_NS, 'link') .attr('href'); var summary_elem = $(this).find('*').ns_filter(ATOM_NS, 'summary').clone(); //Deal with the case of a missing title if (!title_elem.text()){ title_elem = '[No title]'; } //Deal with the case where rel='alternate' is omitted if (!link_text){ link_text = $(this).find('*') .ns_filter(ATOM_NS, 'link') .not('[rel]') .attr('href'); } //Update the target area with the entry information $('<p></p>') .append( $('<a href="' + link_text + '"></a>') .append(title_elem) ) .append(' - ') .append(summary_elem.clone()) .fadeIn('slow') //bonus animation .appendTo('#update-target'); }); //close each( }
同样,我对该文件进行了注释,但是有几点值得特别强调。Atom 有许多可以接受的元素变体,其中多数是可选的。这就意味着您必须对异常情况进行处理。我举两个常见的异常情况:在一个必须的链接上的可选 rel="alternate";标题是可选的这一事实。正如您所看到的,jQuery 提供了处理这些情况的巨大灵活性,因此您甚至应该能够处理这种不规则的 XML 格式。在某些情况下,我直接从 XML 将结构复制到主文档(托管 HTML)。这需要非常小心,并且您会发现我使用了 clone() 方法,以确保我没有将某个文档的节点嫁接到另一个文档,否则 DOM 异常WRONG_DOCUMENT_ERR 将会发出一个错误。另外,我使用了 jQuery 方法 fadeIn,以便添加的内容会慢慢从视线中消失。图 2 是 home.html 的浏览器显示。
jQuery 是所有处理 Web 浏览器问题的技巧和权宜之计的集合,我在本文中介绍的 XML 工作台是面向那些需要处理 XML 的开发人员的可重用工具的第一步。您已经了解到,最大的问题之一就是名称空间处理问题。一旦您越过这个障碍,jQuery 就会给您提供恰当处理用 XML 表达的多种不规则文档的工具。您将会发现处理 Web 提要的技术已经准备好在浏览器中应用于许多其他 XML 格式。
如果您发现 jQuery 和随之产生的权宜之计不合适,那么您还可以更加直接地使用 JavaScript 库进行 XML 处理,例如 Sarissa;这个库值得撰写一篇文章来介绍,但是它没有被广泛使用,也不像 jQuery 那样易于部署。