如今HTML大行其道,但是XML作为标记语言仍然有广泛用途。
XML最有争议的特性之一就是对命名空间(namespace)的支持。C#、Java、C++等语言都有命名空间,这个特性能够使得代码能够更简洁,命名变量的时候更随心所欲;可是,XML中使用命名空间的体验并不佳:
- 任何元素的缺省的命名空间只有一个,也就是说子元素的缺省命名空间会覆盖父元素的缺省命名空间。
- 非缺省命名空间的前缀是必不可少的,可是不像编程语言往往在源文件头部分开定义。本来嘛,应用、声明和定义是不同的东西,但是XML的树形结构非把它们inline掺和在一起,能不乱么。
- 命名空间URI往往是冗长的HTTP地址,对于初学者以及审美要求甚高的互联网开发者而言可谓痛苦不堪。
可见,纵然命名空间是个好特性,但是一个次优的设计多么简单地把开发者给坑了。否则,在互联网的世界中,HTML5怎么会取代XHTML,JSON怎么能跨越XML而大行其道呢?
回过头来,当你不得不使用命名空间的时候,你仍然得弄清其中的差别,少加几天的班。由于历史原因,W3C在开发DOM Level 1的时候命名空间规范并没有开发完毕,所以直到DOM Level 2的时候才加入了对命名空间的支持,这些新的API都以NS结尾,很好认。由于W3C既要保证与DOM Level 1的API兼容,又要在DOM Level 2中加入支持,所以不得不在开发者体验这里做出妥协:混用DOM Level 1中命名空间不敏感的API和DOM Level 2中命名空间敏感的API时,你后果自负。
首先看看在浏览器中混用这些DOM API会得到什么结果吧。
Firefox 6
使用Firefox 6运行以下脚本:
var parser = new DOMParser();
var doc = parser.parseFromString("<root/>", "text/xml");
var at1 = doc.createAttributeNS("url1", "p:at");
var at2 = doc.createAttributeNS("url2", "p:at");
doc.documentElement.setAttributeNode(at1);
doc.documentElement.setAttributeNode(at2);
var serializer = new XMLSerializer();
var str = serializer.serializeToString(doc);
alert(str);
注意哦,我们用支持命名空间的createAttributeNS来创建属性,但是在添加到元素的时候(不小心)使用的是不支持命名空间的setAttributeNode。
结果不无道理:at1和at2虽然各自拥有自己的命名空间,可是作为不知命名空间为何物而只看标签名字的setAttributeNode,把他们认作相同的属性也说得过去。当然这个行为也并不完美:这并不是一个合法的XML结果,因为p并没有定义所对应的URI。
<root p:at=""/>
如果把setAttributeNode换成setAttributeNodeNS,结果如下:
<root p:at="" xmlns:p="url1" a0:at="" xmlns:a0="url2"/>
显然,当使用NS版本API的时候,Firefox正确认识到了at1和at2其实是不一样的属性,所以使用p作为第一个at的前缀,然后自动起了a0作为第二个at的前缀。本来嘛,前缀只是一个代号而已,真正给力的是通过xmlns前缀定义的URL。
Internet Explorer 9
使用setAttributeNode结果如下:
<root xmlns:p="url1" p:at="" xmlns:NS1="url2" NS1:at="" />
使用setAttributeNodeNS结果如下:
<root xmlns:p="url1" p:at="" xmlns:NS1="url2" NS1:at="" />
IE在实现setAttributeNode的时候热心过了头。W3C DOM L3规范说得很清楚:如果遇到相同名字(对属性而言就是它的QName)的节点时,setAttributeNode需要替换前者。出现这样的结果肯定与W3C是有出入的。IE的setAttributeNodeNS实现没问题。
Chrome 13
使用setAttributeNode结果如下:
<root p:at="" xmlns:p="url1" p:at="" xmlns:p="url2"/>
使用setAttributeNodeNS结果如下:<root p:at="" xmlns:p="url1" p:at="" xmlns:p="url2"/>
非常可惜,Chrome对setAttributeNode和setAttributeNodeNS的支持很差,居然生成了不合法的XML内容!歌歌,你这也太业余了吧!
Opera 11
使用setAttributeNode结果如下:
<?xml version="1.0"?><root p:at="" xmlns:p="url2"/>
使用setAttributeNodeNS结果如下:<?xml version="1.0"?><root p:at="" NS1:at="" xmlns:p="url1" xmlns:NS1="url2"/>
好戏压轴——在这个测试中Opera的表现相当完美!
现代的浏览器对XML都有不同程度的支持,虽然略有瑕疵,也不是不能容忍:毕竟,人家处理HTML才是主业,对XML这个副业,稍微也可以打一点酱油。那么我们来看看职业选手的表现吧。
Java 6
下面是使用Java 6中DOM Level 3 Load and Save API写的测试代码:
DOMImplementationLS ls = (DOMImplementationLS)
DOMImplementationRegistry.newInstance().getDOMImplementation("LS");
// Gets a basic document from string.
LSInput input = ls.createLSInput();
InputStream istream = new ByteArrayInputStream("<elem></elem>".getBytes("UTF-8"));
input.setByteStream(istream);
LSParser parser = ls.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null);
Document document = parser.parse(input);
// Creates attributes with namespaces.
Attr a = document.createAttributeNS("url1", "p:at");
Attr b = document.createAttributeNS("url2", "p:at");
Element elem = document.getDocumentElement();
elem.setAttributeNodeNS(a);
elem.setAttributeNodeNS(b);
// Creates a LSSerializer object.
LSSerializer serializer = ls.createLSSerializer();
LSOutput output = ls.createLSOutput();
OutputStream ostream = new ByteArrayOutputStream();
output.setByteStream(ostream);
serializer.write(document, output);
String result = ostream.toString();
最后result字符串的内容为:
<?xml version="1.0" encoding="UTF-8"?>
<elem xmlns:p="url2" p:at=""/>
这个结果在规范与可用性之间有着很好的平衡:正确地替换掉了第一个属性,还加上了URI的定义保证了XML的合法性。
如果把setAttributeNode换成setAttributeNodeNS,结果如下:
<?xml version="1.0" encoding="UTF-8"?>
<elem xmlns:p="url2" p:at="" xmlns:NS1="url1" NS1:at=""/>
完全正确。
.Net 4
下面是测试代码,由于System.Xml更偏好重载以便与其他.Net类库兼容,所以支持命名空间的版本也叫SetAttributeNode而不是SetAttributeNodeNS:
var doc = new XmlDocument();
var a = doc.CreateAttribute("p:a", "url1");
var b = doc.CreateAttribute("p:a", "url2");
var elem = doc.CreateElement("elem");
doc.AppendChild(elem);
elem.SetAttributeNode(a);
elem.SetAttributeNode(b);
System.Xml正确诠释了意图:<elem p:a="" d1p1:a="" xmlns:d1p1="url2" xmlns:p="url1" />
总结一下。由于W3C牺牲了开发者体验,混用NS和非NS版本的API的结果是不可预料的,每个实现都有自己的做法,互操作性根本无从谈起。解决方案看标题:避免混合使用NS和非NS版本的DOM API。其实C#利用重载巧妙地解决了这个问题,可惜如今的JavaScript没有这个机制,W3C也只能退而求其次了。当然,由于时间限制,以上并没有涵盖更多的浏览器引擎与XML类库,但是总体感觉还是有的:术业有专攻,如果要玩XML的命名空间,还是专业的XML类库好!