过去的一年,围绕是否使用新的HTML5语义元素的争论已演变成如何使用新的HTML5语义元素。今年结束前(很多是在本季度结束前)所有主流浏览器都已正式声明支持这些元素,所以是时候开始使用这些元素了。当然,在浏览器的世界里并不是所有的浏览器都支持HTML5,所以如何写出向后兼容的代码就成了一个需要回答的主要问题。
问题
最大的问题就是当使用这些新的语义元素时,那些不支持的浏览器如何处理这些元素。在一个页面中使用HTML5元素主要有三种可能的结果。
- 标签被当做一个错误,并且被完全忽略。构建DOM结构时就像这些标签不存在一样。
- 标签被当做一个错误,并且生成一个作为占位符的DOM节点。就像代码所表示的那样构建DOM,但是标签上没有应用样式(被视为一个内联元素)。
- 标签被当做HTML5的标签,并且生成正确的DOM节点。就像代码所表示的那样构建DOM,并且标签上也应用了合适的样式(许多情况下,被视为一个块级元素)。
下面来看一个具体的例子,考虑下面的代码:
<div class="outer">
<section>
<h1>title</h1>
<p>text</p>
</section>
</div>
很多浏览器(比如Firefox3.6、Safari4)会这样解析这段代码:<div>
作为一个顶级元素,<div>
下有一个不认识的子元素(<section>
),虽然不认识但是<section>
被当做一个内联元素构建在DOM中。<h1>
和<p>
是<section>
的两个子元素。由于<section>
在DOM结构中,所以可以在节点上应用样式。这是第二种情况#2。
IE9以前的IE浏览器会这样解析这段代码:<div>
作为一个顶级元素,将<section>
元素看做一个错误。<section>
会被忽略,<h1>
和<p>
被解析时作为<div>
的子元素。关闭标签</section>
同样也被当做错误忽略。这样解析的效果就等同于下面的代码:
<div class="outer"> <h1>title</h1> <p>text</p> </div>
由此可见,旧的IE浏览器处理未知元素的策略确实能将页面“合理”的恢复,但是相比其他浏览器它构建了一个不同的DOM结构。由于在DOM结构上没有未知的元素,所以你也就不能在<section>
上应用样式。这是一个种情况#1。
当然,那些支持HTML5的浏览器中,比如IE 9、Firefox 4、Safari 5,就能像HTML5规范中规定的那样构建出正确的DOM结构并且能应用正确的默认样式。
因此,最大的问题不仅是浏览器从相同的代码中构建出了不同的DOM结构,同时还为相同的DOM结构应用了不同的样式规则。
解决方案
如今,很多人想出了很多不同的解决方案以使HTML5元素能用于页面上。每个方案都是解决某个或某几个已经提及的特定问题,以实现浏览器的兼容。
JavaScript 垫片(JavaScript shims)
JavaScript shims主要目的是解决HTML5元素在旧IE下的样式问题。在IE中有一个众所周知的怪异行为:IE会忽略掉它不认识的的元素,除非这些元素是通过document.createElement()
创建的。所以如果调用document.createElement("section")
,那么浏览器就会生成<section>
的DOM节点并且在其上应用样式。
像htmlshim[1]这样的shims就是利用这个特性使HTML5元素能在IE中生成DOM节点从而使你能在其上应用样式。Shims通常还会在HTML5块级元素上应用display:block
,从而使其能做到浏览器兼容。
我不喜欢这种方法,因为它违反了我构建web应用的主要原则之一:不应该依赖JavaScript进行布局。这样做不仅仅关系到会给那些禁用JavaScript的用户带来糟糕的体验,而且它还关系到构建可预见的、可维护的、有清晰分层的web app代码库。它确实能在所有浏览器中生成相同的DOM结构并且使你的JavaScript和CSS正确的工作,但在我看来还是弊大于利。
命名空间hack (NameSpace hack)
从来不缺少hacks,还有另一项技巧可以使IE认识那些未知元素。这项技巧第一次获得广泛关注是通过Elco Klingen的 文章,HTML5 elements in Internet Explorer without JavaScript[2]. 这项技巧包括声明一个XML风格的命名空间,然后使用这些元素时加上命名空间前缀,像这样:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:html5="http://www.w3.org/html5/">
<body>
<html5:section>
<!-- content -->
</html5:section>
</body>
</html>
html5
前缀仅仅是个前缀名字而非官方要求,所以你把前缀设置为“foo”效果是一样的。这样使用前缀,Internet Explorer就会认识这些新元素,从而你就可以在它们上面应用样式了。这个方法在其他浏览器中同样能够工作,所以你就能在浏览器中获得相同的DOM和样式。
缺点同样鲜明:你必须在HTML文档中使用XML风格的命名空间,同时也要以相同的风格使用CSS,像下面这样:
html5\:section {
display: block;
}
这并不是我喜欢的编码风格。这是一个漂亮的解决方案,教会我一种不自然的元素应用方式。我不希望看到文件中满是带有命名空间的元素。
防弹技术 ("Bulletproof" technique)
我第一次接触这项技术是在YUIConf2010上,Tantek Celik做了一个主题演讲,HTML5: Right Here, Right Now[3]。在那个演讲中,Taktek建议在每个HTML5的块级元素中内嵌一个<div>
,在这个<div>
上设置一个CSS类用以表示它所代表的HTML5元素。例如:
<section>
<div class="section">
<!-- content -->
</div>
</section>
这种方法的目的是使内容能在所有浏览器中保持正确的文档流。在一个HTML5的块级元素中嵌入一个块级元素意味着会有三种情况:你要么有一个单独的块级元素(Internet Explorer < 9),要么是一个块级元素包含在一个内联元素中(Firefox3.,Safari,etc),或者是一个块级元素包含在一个块级元素中。无论哪种情况,默认的渲染都是一样的。
Tantek提出了这种方法的一个例外情况,就是<hgroup>
。<hgroup>
明确指出不允许非head元素作为其子元素。为此,他推荐将<div>
放在外边:
<div class="hgroup">
<hgroup>
<!-- content -->
</hgroup>
</div>
关于样式,Tantek推荐不要试图去给HTML5元素本身应用样式,而是要给作为代理的<div>
应用样式。所以不要这样做:
section {
color: blue;
}
而是这样做:
.section {
color: blue;
}
这样做的理由是,以后它会很容易的自动转换成一个参照这个模式的HTML5元素的标签。我并不热衷于他的这个建议,因为我通常不喜欢通过标签名称应用样式。
这个方法的缺点是会在不同的浏览器中生成不同的DOM结构,所以你在写JavaScript和CSS时就要小心了。例如,使用孩子选择器(>)时经过HTML5的元素时,就不能做到在所有的浏览器中工作正确。同时,直接访问parentNode
也会在不同的浏览器中获得不同的节点。这种情况在下面这样的代码中尤为明显:
<div class="outer">
<section>
<div class="section main">
<!-- content -->
</div>
</section>
</div>
比如你有个选择器section > .main
,它在Internet Expl 8和就早的版本中就不会工作。
反转防弹技术(Reverse bulletproof technique)
有另外一些文章,像Thierry Koblentz的HTML elements and surrogate DIVs[4]. 展示了反转Tantek的方法:将HTML5元素包含在<div>
中。例如:
<div class="section">
<section>
<!-- content -->
</section>
<div>
唯一的不同之处就是HTML5元素所在的位置——所有的元素都在相同的位置。支持者喜欢这项技术是因为他的一致性(对所有元素来说工作方式是一样的,包括<hgroup>
)。 值得指出的是,像Tantek的方法一样,它同样会有CSS选择器适用性和JavaScript DOM访问的问题。它的主要优点就是一致性。
我的方法(My approach)
在方法的选择上我的主要目标是:这个方法要使我仅仅改变页面上的HTML。这意味着对于CSS和JavaScript来说要0修改。为什么要做出这样的决定呢?对于web应用(或者其他应用)来说,你的变化所涉及的层越多,引入bug的风险就越高。将变化限制在一层那么就一定程度上限制了bug的引入,而且如果一旦出现bug,你就可以只在一个领域去寻找底层问题的所在。例如,如果布局被破坏了,我就知道这是因为我增加了<section>
元素,而不是其他的什么CSS和JavaScript问题。
在调研了这些技术后,我做了一些原型和测试,最终我还是回到了Tantek的方法上。在不需要修改CSS和JavaScript的前提下,这是使我现有的原型页面正常工作的唯一方法。不过,我并没有完全按照他的方法来做,而是做了些变化,我认为这些变化对这个方法有所改进。
第一点,我从不在代表HTML5元素的类上加任何样式(所以在我的选择器中没有.section
)。我保持页面中已有的<div>
元素,为它使用语义类名作为应用样式和JavaScript的钩子。例如,下面的代码:
<div class="content">
<!-- content -->
</div>
就变成了这样:
<section>
<div class="section content">
<!-- content -->
</div>
</section>
有了这点变化,我仍然使用.content
作为样式和脚本的钩子。这样,我已有的JavaScript和CSS就不需要修改了。
第二点,与其将<hgroup>
作为一种特殊情况,我选择不去使用它。事实是,我在我现有的页面中找不到使用这个元素的场景。
为了从中选出一个较优的方案,我花费了大量时间在防弹技术和反转防弹技术的比较上。对于我来说,做出选择的关键因素是反转防弹技术需要我增加CSS以使它能正常的工作。在那些为HTML5元素生成DOM节点但是没有应用默认样式的浏览器中,将HTML5块级元素嵌套在<div>
中不止一次的打乱了我的布局,因为在这些老浏览器中这些HTML5块级元素变成了内联元素。我必须显示的添加规则将它们变成块级元素以使我的布局能够工作,但是这样的话就不符合我的初衷了:不要修改CSS。
证明(The proof)
在讨论时我发现的一件令人无比沮丧的事就是人们太轻易就否定一种方法了,因为他们总能找出至少一个反例。我在本文中展示的每一种方法都不是完美的;每一种方法都不能适用于你遇到的所有情况。如果你提供给我一项技术,我敢保证肯定有人能找出一个它不能工作的情况。但是这并不能否定这项技术的价值,它仅仅是告诉你这项技术有它的局限性,你可以做出更好的选择。
在我的调研中,我使用一些已经存在的页面并使用“改进的防弹技术”修改它们。我将它们放入带有简单布局的页面、带有复杂布局的页面、带有JavaScript交互和不带有JavaScript交互的页面。每一种用例下,我唯一需要修改的就是HTML并且一切都能正确工作(不需要修改CSS和JavaScript)。那些关于子节点和父节点关系的警告呢?有意思的是我从来没有碰到那些问题。
对于我来说比较容易的原因可能是我代码写的比较严苛。我认真的复查:
- 不用标签名和IDs应用样式(只用类)
- CSS选择器尽可能一般化,选择器类型尽可能少
- JavaScript不依赖特定的DOM结构
- 不用标签名操作DOM
另一件有趣的事是我使用HTML5作为容器。这些元素其实仅仅是功能组(功能块)之间的界限。你在边界内使用样式和脚本,而不是穿越这些边界本身。由于在边界内使用那些元素的JavaScript和CSS能够良好的工作,所以我猜想那些编码良好的站点也能够很好的使用这个方法。
结论
我最后选择的这项技巧是修改Tantek的防弹技术得到的,同时我也将他推荐给其他人。很明显,这个名字有点用词不当,因为它对CSS和JavaScript有一些副作用,但是在我的经验中它确实是唯一允许我只修改页面中的HTML而其他部分能继续工作的方法。我肯定争论还会继续,不管是在各公司的内部还是互联网上。我希望本文能对你做出决定提供帮助。
引用(References)
- html5shim
- HTML5 elements in Internet Explorer without JavaScript , by Elco Klingen
- HTML5: Right Here, Right Now , by Tantek Çelik ( Video , Slides )
- HTML elements and surrogate DIVs , by Thierry Koblentz