现在HTML5:通过Polyfills获得更多
戴夫沃德 | 2012年5月17日
利用 HTML5 来搭建网站和应用可能是一项艰巨的任务。尽管现在越来越多的现代浏览器正在更多的支持Html5新特性,但实际上只有很少部分人能够幸运的只需要为这些最新的浏览器编写代码。作为一个专业的开发者,你必须要花很多精力来调整不自由的空间排版和实现承诺过的特性以及面对现在的现实情况,这些都是因为浏览器的碎片化。好消息是 IE 9 和 10 都已经支持HTML5 了,用户可以抛弃旧版的 Internet Explorer 浏览器了,不过对于开发者而言他们还需要考虑支持旧版的浏览器。
但是,这并不意味着您不得不放弃在短期内支持HTML5。就像网站有技巧支持多种屏幕尺寸和不同级别的CSS功能之类的差异一样,也可以实现令人惊讶的强大的跨浏览器HTML5支持。尽管较旧的浏览器缺少许多HTML5的新API,但JavaScript是一种非常灵活的语言,并且提供了追溯性地添加新功能的机会,当它们不是本地存在时。
跨浏览器支持
跳跃到HTML5最让人头痛的问题是,我们大多数人别无选择,只能支持各种对最有用的新API几乎或不支持的旧浏览器。采用新的Web技术的想法让人联想到了跨浏览器不一致性,不可维护的分支代码,浏览器嗅探以及其他一系列问题的恶梦。但是,有一项评估不足的技术可以完全缓解HTML5某些新功能的这些问题,并且仍然允许您针对新的API进行开发,就好像您的所有用户在一夜之间都升级了浏览器:polyfills。
Polyfilling是Remy Sharp创造的一个术语,用于描述以重复缺失API的方式回填缺失功能的方法。使用这种技术,您可以编写特定于应用程序的代码,而不用担心每个用户的浏览器是否本机实现它。事实上,polyfills不是一种新技术,也不是绑定到HTML5。多年来,我们一直在使用诸如json2.js,ie7-js之类的polyfills,以及在Internet Explorer中提供透明PNG支持的各种回退。不同之处在于去年HTML5 polyfills的扩散。
什么使Polyfill?
有关我正在谈论的具体示例,请查看json2.js。具体来说,这是JSON.parse实现中的第一行代码:
if(typeof JSON.parse!== 'function'){ // Crockford的JSON.parse的JavaScript实现 }
通过使用typeof测试守护代码,如果浏览器具有JSON.parse的本机实现,则json2.js不会试图干扰或重新定义它。如果本机API不可用,那么json2.js将以与JavaScript本机JSON API完全兼容的方式实现JSON.parse的JavaScript版本。最终,这意味着您可以在页面中包含json2.js,并且有信心使用JSON.parse,而不考虑您的代码运行在哪个浏览器中。
这显示了polyfilling方法的优点 - 不仅提供了兼容性层,还提供了一种试图密切反映polyfill实现的标准API的方式。因此,没有一个站点特定的代码需要知道或关心兼容层的存在。最终,这会产生更干净,更简单的特定于应用程序的代码,可让您利用新的API,同时仍保持与旧版浏览器的兼容性。
HTML5的新语义元素
HTML5 中对于polyfil来说最简单的特性就是设置已经增加了的语义元素,如<article>,<aside>,<header>和<time>。他们中的大多数和<div>,<span>的表现没有区别,但是它们有自己语义化的意义。因为这些元素是标准通用内置语言(SGML),所以好处就是像 IE6 这样的旧浏览器也能够显示他们。不过 IE浏览器的奇怪之处就是它只应用那些它承认的 CSS 样式。因此,即使旧的 IE浏览器显示了 HTML5 的新语义元素,但是它仍会忽视那些用户自定义的样式。
幸运的是,Sjoerd Visscher 为 IE 找到了一个简单的解决方法,John Resig 又让它发扬光大。在使用任意元素形式的时候调用 document.createElement(),这样就可以让 IE 浏览器运用 CSS 中所有的样式了。
例如,在 <head> 中单独调用 document.createElement(‘article’)就可以让 IE 浏览器强制运用CSS中的 <article> 元素。
1 <html>
2 <head>
3 <title>HTML5 Now?</title>
4 <style>
5 article { margin: 0 auto; 960px; }
6 </style>
7 <script>
8 document.createElement('article');
9 </script>
10 </head>
11 <body>
12 <article>
13 <!-- TODO: Write article… -->
14 </article>
15 </body>
16 </html>
图1这个对document.createElement的调用改变了Internet Explorer应用CSS样式的方式。
当然,没有人愿意为HTML5中添加的每种新语义元素手动添加createElement语句。把这种单调乏味的东西抽象出来就是一个polyfill发光的地方。在这种情况下,有一个称为html5shim(也称为html5shiv)的polyfill,它可以自动执行初始化Internet Explorer与新语义元素兼容性的过程。
例如,图1中的代码可以被重构为使用html5shim,如图2所示。
1 <html>
2 <head>
3 <title>HTML5 Now!</title>
4 <style>
5 article { margin: 0 auto; 960px; }
6 </style>
7 <!--[if ltIE 9]>
8 <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
9 <![endif]-->
10 </head>
11 <body>
12 <article>
13 <!-- TODO: Write article… -->
14 </article>
15 </body>
16 </html>
图2使用html5shim填充
注意围绕脚本引用html5shim的条件注释。这确保了polyfill只会在早于版本9的Internet Explorer版本中加载和执行。没有时间浪费下载,解析并在已为新元素提供适当支持的浏览器中执行此代码。
另一种选择考虑
如果您对HTML5有兴趣阅读本文,您可能已经意识到或使用Modernizr。但是,您可能不知道的一件事是Modernizr具有内置的html5shim的createElement功能。如果您使用Modernizr进行功能检测,则您已经对HTML5的语义元素具有向后兼容性。
持久的客户端存储
多年来,我们别无选择,只能将特定于供应商的DOM扩展和专有插件组合在一起,以解决在浏览器中长期持续存在的问题。这些解决方案包括Firefox的globalStorage,Internet Explorer的userData,Cookie和插件,如Flash或Google Gears。虽然可行,但这些被黑客入侵的解决方法很繁琐,难以维护,并且容易出错。
为了解决这个问题,HTML5中最受欢迎的补充之一是基于标准的API,用于在浏览器中持久存储数据:localStorage。此存储API提供了一致的客户端 - 服务器键/值存储,可以为用户访问的每个网站存储最多5 MB的隔离数据。您可以将localStorage视为一个容易处理的巨大cookie,并且在每个HTTP请求期间不会在浏览器和服务器之间来回传送。localStorage功能非常适合需要浏览器特定数据的任务,如记忆首选项和本地缓存远程数据。
每个A级浏览器都支持localStorage功能,其中包括Internet Explorer 8,但在大多数浏览器的旧版本中缺少该功能。与此同时,几种解决方案已经发展到跨浏览器存储到那些旧版浏览器中。它们包括Remy Sharp的存储polyfiller的简单性,以及store.js和PersistJS提供的全面向后兼容性,以及LawnChair和AmplifyJS存储模块的全功能API 。
例如,您可以使用AmplifyJS存储模块在用户的浏览器中保存一些数据,而无需使用Cookie - 即使该用户使用的是Internet Explorer 6:
1 // Sets a localStorage variable 'Name'with my name in it.
2 amplify.store('name','Dave Ward');
3 var website ={
4 name:'Encosia',
5 url:'http://encosia.com'
6 }
7 // The library takes care of serializingobjects automatically.
8 amplify.store('website', website);
在稍提取数据的时候变得非常容易:
1 // The values we stored before could thenbe used at a later time, even
2 // during a different session.
3 var $personLink = $('<a>',{
4 text: amplify.store('name'),
5 href: amplify.store('website').url
6 });
7 $personLink.appendTo('body');
同样,关于使用localStorage或基于localStorage的API的好处是,这些数据都不需要保存在cookie中,然后与每个HTTP请求一起传输,也不需要调用像Flash这样的重量级插件只是为了存储一些数据。数据存储在一个真正的,孤立的本地存储机制中,这对于本地缓存数据或开发对离线使用有着丰富支持的站点非常有用。
使用什么?
Remy Sharp的存储polyfiller是唯一真正符合polyfill的存储polyfiller,因为其他人不完全模仿HTML5 localStorage API。但是,store.js和AmplifyJS存储模块支持更广泛的回退方法,以实现旧版浏览器的兼容性。实际上,这很难忽视。
地理位置
地理定位是另一个HTML5功能,可以用于填充。如果浏览器和操作系统都支持地理定位并且正在使用GPS传感器的设备上运行,则HTML5将提供对地理位置API的访问权限,以允许JavaScript代码确定您的页面正在被访问的位置。
移动设备是基于浏览器的地理位置最令人印象深刻的例子。通过将其内置的GPS硬件与支持HTML5地理定位API的现代浏览器相结合,Android和iOS设备都能以原生应用程序的精确度支持原生HTML5地理定位。
在这些最佳环境中访问地理位置数据所需的JavaScript就像这样简单:
1 navigator.geolocation.getCurrentPosition(function(position){
2 var lat =position.coords.latitude;
3 var long =position.coords.longitude;
4 console.log('Current location: ', lat, log);
5 });
这对移动应用来说非常好,但桌面硬件通常不包含GPS传感器。然而,我们都习惯了位置感知广告,它们一直在互联网上跟踪我们在桌面硬件上的时间,远远超过地理定位API的存在时间,所以显然可以解决桌面浏览环境中缺乏GPS的问题。
在JavaScript中,目前的解决方法是在已知IP位置的数据库中查找访问者的IP地址。这种方法的准确度远低于使用GPS设备的准确度,但这些数据库通常能够在正确的区域范围内定位IP地址,这对于许多应用来说是足够有用的。
您可能会注意到不会仅依靠IP地址查找的更精确的无GPS定位技术。通常,这些增强的估计是通过将可见Wi-Fi热点标识符与热点的这些特定组合已经物理位于过去的数据库进行比较的新颖方法来完成的。
不幸的是,在浏览器中运行的JavaScript代码目前不知道来自操作系统的数据。因此,在可预见的将来,基于Wi-Fi的技术不适用于polyfills,因此我们只能将IP查找作为唯一选择。
Paul Irish编写了一个简单的地理定位polyfill,在旧版浏览器和缺少GPS传感器的硬件上提供了一定程度的地理定位。它通过使用Google的地理位置API将访问者的IP地址转换为近似的物理位置来实现此目的。这是一个真正的polyfill,它将地理定位功能插入到navigator.geolocation对象中,但前提是浏览器本身不提供地理定位API。
浏览器历史和导航
由于肤浅的DHTML效果让位于更加结构化的客户端功能,如基于AJAX的分页和单页面界面,这些结构更改开始与浏览器的内置导航和历史记录功能不同步。然后,当用户直观地尝试使用其“后退”按钮导航到前一页或应用程序状态时,情况变得糟糕。搜索“禁用后退按钮”显示了此问题困扰现代Web开发的程度。
操纵浏览器位置的“哈希”部分有助于解决问题的一个方面。由于哈希最初是为了在同一页面内的导航点之间跳转而发生的,因此更改URL的哈希值不会像更改基础URL前缀那样触发页面刷新。利用散列属性允许客户端更新,以保持浏览器的显示地址与没有传统导航事件发生的JavaScript驱动更改同步。
onhashchange事件
尽管操纵浏览器的哈希值得到了很好的支持,但即使超过Internet Explorer 6,监视哈希变化的标准化方法直到最近才变得更加难以捉摸。当前的浏览器作物支持onhashchange事件,当地址的哈希部分发生变化时触发onhashchange事件 - 完美用于检测用户何时尝试通过使用浏览器的导航控件浏览客户端状态更改。不幸的是,onhashchange事件只在相对较新的浏览器中实现,支持从Internet Explorer 8和Firefox 3.6的版本开始。
尽管onhashchange事件在旧版浏览器中不可用,但有些库在旧版浏览器中提供了抽象层。这些兼容性Shim使用特定于浏览器的怪异来复制标准的onhashchange事件,即使采取每秒多次监视location.hash的方式,并且在浏览器中更改时没有其他方法。
Ben Alman的jQuery Hashchange插件就是其中的一个可靠选择,他从他流行的jQuery BBQ插件中提取了插件。Alman的jQuery Hashchange公开了一个具有非常深入的跨浏览器兼容性的hashchange事件。我毫不犹豫地称它为一个polyfill,因为它需要jQuery,并不完全重复本机API,但如果你已经在你的页面上使用jQuery,它会很好用。
超越HashState
操作哈希是解决客户端状态管理问题的良好开端,但它并非没有缺点。劫持合法的浏览器导航功能不是最佳选择,因为基于散列的URL结构可能会导致用户混淆并与现有的页内导航发生冲突。
一个更基本的问题是,浏览器不会在HTTP请求中包含请求的URL的哈希部分。如果不访问URL的这一部分,就不可能立即返回与用户加入书签,通过电子邮件接收或通过社交分享发现的页面处于相同状态的页面。这导致网站别无选择,只能在默认的初始状态下显示页面,然后自动触发一个令人震惊的转换,直到用户真正需要的状态。为了找到这种影响可用性的证据,除了对Twitter和Gawker Media的“hash bang”重新设计广泛的负面反应之外,您还需要寻找其他方面。
输入pushState
幸运的是,HTML5还引入了一对更高级的API,可显着改善客户端历史管理情况。通常简称为pushState,window.history.pushState方法和window.onpopstate事件的组合提供了异步处理浏览器地址的整个路径部分的途径,并且同样对哈希之外的导航事件做出反应。
在GitHub上浏览项目的源代码是当前使用pushState最好的真实世界示例之一。由于使用pushState操纵浏览器的地址不会像传统的地址更改那样导致整个页面的刷新,所以GitHub能够在每个代码的“页面”之间提供动画转换,同时仍然保留用户友好的URL哈希或querystrings。
更好的是,如果您将书签保存到这些URL之一并稍后直接导航到其中,则GitHub能够在第一个请求中立即向您提供正确的内容,因为客户端URL结构与它们在服务器上使用的内容相匹配。正如我前面提到的,当你使用基于散列的URL时,这样做是不可能的,因为你的Web服务器永远不会知道请求的散列部分。
在自己的代码中使用onhashchange和pushState
不幸的是,要真正将pushState功能填充到不支持它的浏览器中是不可能的。没有抽象层可以改变修改旧版浏览器中的URL会触发页面加载的事实。但是,通过在实现浏览器的pushState中使用pushState,然后在旧版浏览器中使用基于散列的方法,您可以拥有两全其美的功能。
本杰明·卢普顿(Benjamin Lupton)组建了一个优秀的跨浏览器库,以平滑与保持客户端历史记录一起出现的各种各样的怪癖和不一致性。他的图书馆涵盖从Internet Explorer 6到最新版Chrome的所有浏览器。使用库很简单。它有一个紧跟HTML5自己的pushState语法的语法:
1 // This changes the URL to /state1 in HTML5 browsers, and changes it to
2 // /#/state1 in older browsers.
3 History.pushState(null, 'State 1','state1');
4 // Same as before, but /state2 and/#/state2.
5 History.pushState(null, 'State 2','state2');
history.js并没有提供HTML5 popstate事件的确切副本,而是包含了各种适配器来处理这些库中的事件处理系统。例如,使用jQuery适配器,您可以将事件处理程序绑定到history.js statechange事件,如下所示:
1 History.Adapter.bind(window,'statechange',function(){
2 // Get the newhistory state from history.js.
3 var state =History.getState();
4 // Write the URLwe’ve navigated to on the console.
5 console.log(state.url);
6 });
这个statechange事件处理程序在浏览器浏览通过history.js pushState方法持续存在的历史点时触发。无论是在原生支持pushState的HTML5浏览器中,还是在仅支持基于散列的URL更改的旧版浏览器中,监视此单一事件都会捕获任何活动。
把它用于真实世界的应用程序很容易。您大概可以想象将它与AJAX驱动的网格分页和排序结合使用,甚至可以用于整个网站(例如Gmail或Twitter)的导航,而无需诉诸于那些普遍讨厌的散列网址和重定向。
用pushScissors运行
使用pushState时需要注意的一件事是,您必须注意,您的服务器将正确响应您在客户端使用的每个URL。由于构建客户端URL很容易,服务器将以404或500错误(例如,/ undefined)响应,因此确保将服务器端路由或URL重写配置为尽可能优雅地处理意外的URL。例如,如果您在/ report上有多页报表,并且每个页面上的pushState驱动的URL为/ report / 2,/ report / 3等,则应确保您的服务器端代码正常响应/ report / undefined等网址。
不太理想的替代方法是在pushState地址更新中使用查询字符串URL片段,例如/ report?page = 2和/ report?page = 3。由此产生的URL看起来不太好,但它们至少不太可能导致404错误。
从这往哪儿走
本文只是抓住了HTML5 polyfills生态系统的表面。有一些活跃的项目为SVG和canvas图形,HTML5视频,ECMAScript 5甚至WebWorkers等功能提供跨浏览器支持。如果您有兴趣了解更多关于这些项目的信息,请参阅以下简要说明以及其中许多内容的链接:https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills