利用 HTML5 來搭建網站和應用可能是一項艱巨的任務。儘管現在越來越多的現代瀏覽器正在更多的支持Html5新特性,但實際上只有很少部分人能夠幸運的只需要為這些最新的瀏覽器編寫代碼。作為一個專業的開發者,你必須要花很多精力來調整不自由的空間排版和實現承諾過的特性以及面對現在的現實情況,這些都是因為瀏 ...
利用 HTML5 來搭建網站和應用可能是一項艱巨的任務。儘管現在越來越多的現代瀏覽器正在更多的支持Html5新特性,但實際上只有很少部分人能夠幸運的只需要為這些最新的瀏覽器編寫代碼。作為一個專業的開發者,你必須要花很多精力來調整不自由的空間排版和實現承諾過的特性以及面對現在的現實情況,這些都是因為瀏覽器的碎片化。好消息是 IE 9 和 10 都已經支持HTML5 了,用戶可以拋棄舊版的 Internet Explorer 瀏覽器了,不過對於開發者而言他們還需要考慮支持舊版的瀏覽器。
不管怎樣,這並不是說你必須要在接下來的時間里放棄使用 HTML5,好在網站可以使用很多技術來優雅的支持多種不同的情況,如多種屏幕尺寸和不同的 CSS 功能,這些科技也可以達到讓人驚訝的完善的跨瀏覽器的 HTML5 支持。儘管舊瀏覽器缺少很多 HTML5 新 API ,不過JavaScript 是一個不可思議的修複語言,可以追加很多瀏覽器原生不支持的特性
跨瀏覽器支持
大量使用 HTML5 最棘手的問題是我們別無他選的要支持舊的瀏覽器,而他們對新的 API 有的支持很少甚至有些根本不支持。適應新的網頁技術讓我們對跨瀏覽器差異不寒而慄,還有難以維護的分支代碼、瀏覽器探測和其他一堆問題。儘管如此我們還有一個低調的技術,它可以將這些問題緩解一些,雖然說不定你的用戶過了一晚就已經把他們的瀏覽器升級成最新的了,這個功能就是:polyfills。
Polyfilling 是由 RemySharp 提出的一個術語,它是用來描述複製缺少的 API 和API 功能的行為。你可以使用它編寫單獨應用的代碼而不用擔心其他瀏覽器原生是不是支持。實際上,polyfills並不是新技術也不是和 HTML5 捆綁到一起的。我們已經在如json2.js,ie7-js 和為 IE 瀏覽器提供透明 PNG 支持的 JS 中使用過了。而和現在 polyfills 的區別就是去年增加的 HTML5 polyfills。
Polyfill 是什麼?
為了具體說明我想要說的,我們來看一下 json2.js。特別註意第一行的 JSON.parse :
1 if (typeof JSON.parse !=='function') {
2 // Crockford’s JavaScript implementation of JSON.parse
3 }
使用 typeof 測試這段代碼,如果瀏覽器有原生的執行 JSON.parse,那麼 json2.js 就不會去干擾或者重定義它。如果原生的 API 不可用,json2.js 就會執行一段 JavaScript 腳本來實現,它和原生的 JSON API 是完全相容的。最後你可以在網頁上使用 json2.js 而且不用考慮瀏覽器運行的是哪種代碼。
這顯示出了 polyfilling 的優勢 - 不僅是相容方面,更是提供了一種接近標準 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; width: 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 改變了 IE 瀏覽器應用的 CSS 樣式
不過,沒人願意每次都為 HTML5中那麼多新增的語義元素手動添加 createElement 聲明。而這就是 polyfill 最擅長的。有一個叫做 html5shim(也就是 html5shiv) 自動完成了設置 IE 瀏覽器和新語義元素的相容性。
舉例說明,示例1中的代碼可以用html5shim 來重構,見示例2.
1 <html>
2 <head>
3 <title>HTML5 Now!</title>
4 <style>
5 article { margin: 0 auto; width: 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 polyfill
註意腳本周圍的描述是參考 html5shim的。這保證了這個 polyfill 只會在比 IE9 更舊的 IE瀏覽器上載入。而其他已經支持新語義元素的瀏覽器則不需要浪費時間下載,暫停或者執行。
另外一個可考慮的選擇
如果你對 HTML5 非常感興趣而在讀這篇文章,相信你可能已經瞭解使用 Modernizr 了。不過你可能還不知道 Modernizr 其實已經內建了 html5shim 功能。如果你是為了探測特性而使用 Modernizr,那麼你已經落後於 HTML5 新特性的相容性了。
持續的客戶端存儲
很多年來,我們除了一起解決廠商專有的 DOM 擴展和自有的插件來保持瀏覽器的長期狀態的問題別無他選。這些解決方案中就包括火狐的 globalStorage,IE 的userData,cookie 和 像 Flash 或者Google Gears。雖然這些方案是可行的,不過這些解決方法都很過時,乏味,難以維護而且易出錯。
為了彌補這些問題,HTML 中最受歡迎的擴展之一就是瀏覽器數據長期存儲的標準 API:localStorage。這個API 提供了一個持續的客戶端/伺服器端的 key/值存儲,它可以為每個用戶訪問的每個網站存儲最多5 MB 的獨立數據。你可以把 localStorage 當做一個易用的龐大 cookie而且不需要每次都不需要瀏覽器和伺服器進行 HTTP 請求。localStorage 特性是那些需要瀏覽器專有數據項目最完美的搭檔,就像記住偏好和本地緩存的遠程數據。
現在所有高級的瀏覽器都已經支持 localStorage 特性了, IE8 也包括在其中,不過在比 IE8 舊一些的版本中就不支持了,同時,還是有 polyfill 的跨瀏覽器存儲可以讓舊版的瀏覽器也支持這樣的特性。從簡樸的 RemySharp 的 Storage polyfiller 到可以向下相容的 store.js 和 PersistJS,還有 LawnChair 的全功能 API 和 AmplifyJS 存儲模塊。
例如,下麵你可能是使用 AmplifyJS 存儲模塊來將數據保存在用戶瀏覽器中而且不藉助 cookies - 就算那位用戶使用的是 IE6:
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);
Pulling that dataout at a later date becomes extremely easy:
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 的優勢是數據不需要經過每次 HTTP 請求,也不需要調用像 Flash 這樣的重量級的插件來存儲數據。數據被保存在一個真實的,獨立的本地存儲機制中,這讓緩存數據到本地和開發需要良好支持離線使用的網站變得很順手。
使用什麼
Remy Sharp 的Storage Polyfiller 是唯一一個可以有資格作為 polyfill 使用的,因為其他的都不能完美的模仿 HTML5 localStorage API。不過不管怎樣,store.js 和 AmplifyJS 存儲模塊提供了很大範圍的舊瀏覽器相容支持,這點很難被忽視。
地理位置
地理位置是另一個成熟的 polyfill 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 存在的時候長多了。因此在缺乏硬體支持的桌面瀏覽環境下獲取地理位置也是可以的。
JavaScript 目前的做法是在已知的 IP 位置庫中尋找訪問者的 IP 地址。這種做法顯然沒有使用 GPS 設備精確,不過這些資料庫通常能夠準確定位區域位置,這對於很多應用來說已經足夠了。
你或許已經知道無GPS 的更精確的地理位置定位不僅僅依賴查找 IP 地址。 通常來說這些增強定位準確性的方法都是藉助 Wi-Fi 熱點的位置庫來協助完成的。不幸的是,目前瀏覽器中運行的 JavaScript代碼不能夠從系統底層調用信息。所以, polyfill 目前不可以使用基於 Wi-Fi 的技術,我們只可以使用 IP查找來代替。
Paul Irish 寫了一個可以為舊瀏覽器和缺少 GPS 感測器的硬體提供定位的簡單的地理位置 polyfill。它使用了谷歌的地理位置 API 來將用戶的 IP 地址轉換成相近的物理地理位置。它是一個真實的 polyfill,它將地理位置功能加入到了 navigator.geolocation對象中,不過只是在瀏覽器原生沒有提供地理位置 API 的情況下使用。
瀏覽歷史和導航
簡單的DHTML效果提供了更加結構化的客戶端特性,例如基於 AJAX 的分頁和單頁界面,這些結構變化放棄了和瀏覽器內置的導航和歷史功能同步。當用戶很自然的嘗試用他們的返回按鈕回到上一頁頁面或者應用的狀態的時候,事情就不那麼好了。我們搜索“禁用返回按鈕”就會發現這個問題對現代網頁開發的壞影響有多少了。
操作瀏覽器地址的“Hash”部分可以幫助我們解決部分問題。因為Hash原本就是用來在同一個頁面中在不同的導航點之間跳轉,更改鏈接的Hash值不會讓頁面像更改到相關的鏈接首碼那樣刷新。利用Hash值允許客戶端和JavaScript驅動的改變同步來保持瀏覽器顯示的地址,這樣就不需要使用傳統的導航事件了。
Onhashchange 事件
當操作瀏覽器Hash部分被很好的支持了,甚至連在 IE6 上都是,直到最近一個標準的監控Hash變化的方法才變得難以捉摸。最近的新瀏覽器支持了 onhashchange 事件,當地址的Hash部分改變的時候它就被觸發了 - 可以完美的檢測用戶想通過瀏覽器的導航控制改變客戶端狀態的情況。可惜的是,onhashchange 事件只有相對較新的瀏覽器才支持,從 IE8 和 Firefox 3.6 之後都支持。
雖然 onhashchange 事件並不支持舊的瀏覽器,不過有可以為舊瀏覽器提供一個抽象的層的庫文件。這些相容的層使用瀏覽器特有的屬性來複制標準的 onhashchangge 事件,甚至可以每一秒監控 location.hash 好多次,並且當地址Hash值在瀏覽器中改變的時候作出響應。
一個不錯的選擇是 Ben Alman 的 jQuery Hashchange 插件,這是他從自己開發的很流行的 jQueryBBQ 插件中提取出來的。 Alman 的 jQueryHashchange 提供了一個非常深層的跨瀏覽器的 hashchange 事件相容性。我有點猶豫要不要把它稱為 polyfill,因為這需要 jQuery 而且不能夠準確的複製出原生 API ,不過當你的頁面已經使用了 jQuery 的時候它真的很棒!
超越 HashState
操作Hash值是一個解決客戶端狀態管理問題的好方法,不過它還是有缺點的。自從基於Hash值的鏈接會讓用戶迷糊並且和頁面上已有的導航衝突,控制瀏覽器導航特性就不是最好的方法了。
一個更根本的問題是瀏覽器並沒有將Hash部分的瀏覽器請求加入到 HTTP 請求中。沒有訪問鏈接中的Hash位置,所以沒可能立即返回到用戶給頁面加書簽、通過郵件接收或者通過社交網路分享後相同的狀態。這導致了網站只可以顯示它們的預設頁面,初始狀態接著再通過一個彆扭的轉換跳轉到期望的位置。為了證明這點在使用性上的影響,你只要看一下 Twitter 和 Gawker Media 的 “hash bang”重設計。
輸入 pushState
幸運的是,HTML5 引入了一對明顯提高了客戶端歷史管理方案的更先進的 API。通常被稱為 pushState,它和 windows.history.pushState 方法和window.onopstate 事件結合起來的,它提供了非同步處理整個瀏覽器地址路徑部分和在對Hash之外的導航事件響應的方法。
在 Github 上查看項目的代碼是現實中正在使用 pushState 最佳的示例。因為通過 pushState 處理瀏覽器的地址不會像傳統地址改變那樣刷新整個頁面,Github 可以在每個代碼頁面切換之間提供過渡動畫,鏈接還是用戶友好的,而不是Hash標簽或者查詢字元串。
更好的是,如果你將其中一個鏈接保存為書簽,之後再訪問這個鏈接的時候,Github 會在你第一次請求的時候就立刻給你正確的內容,因為客戶端中的鏈接就和伺服器端的鏈接結構是一樣的。就像我前面提到的那樣,使用基於Hash的鏈接是不可能實現這些的,因為伺服器和Hash部分的請求是無關的。
在你的代碼中使用 onhashchange 和 pushState
可惜的是,要想將瀏覽器不支持的 pushState 特性通過真正的 polyfill pushState 加入進去是不可能的。沒有抽象層可以改變在舊瀏覽器中改變鏈接會讓瀏覽器跳轉和載入頁面的事實。不過你可以在支持 pushState 的瀏覽器中使用它而在不支持的舊瀏覽器中使用基於Hash部分的鏈接。
Benjamin Lupton 組建了一個很棒的跨瀏覽器庫,可以有效的解決在管理客戶端歷史時候遇到的大範圍的詭異和不一致的現象。這個庫可以用在從 IE6 到最新版的 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');
相比準確複製 HTML5 popstate 事件,history.js 包含了許多種類的適配器可以和那些庫里的事件系統協調運行。例如,使用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 });
通過 history.js 的pushState 方法,statechange 事件處理程式會在每次瀏覽器導航到通過 history.js 的 pushState 方法維持的歷史節點的時候觸發。不論是原生就支持 pushState 的 HTML5瀏覽器 還是僅支持基於鏈接Hash部分改變的舊瀏覽器都會監控這個事件,捕獲每次活動。
將這個運用到現實應用中非常簡單。你可以想象到將它用在和 AJAX 提供的表格分頁和排序中,或者甚至是整站的導航(例如 Gmail 和 Twitter),它們不需要依靠那些大家都很厭惡的Hash鏈接和重定向。
使用 pushScissors 運行
使用 pushState 有一件需要註意的事,那就是你必須保證伺服器端可以正確的你在客戶端適應的每個鏈接。因為很容易你建立一個客戶端的鏈接你的伺服器用一個 404 或者 500 錯誤響應(舉例,/未定義),這樣很好的保證了你的伺服器端在發送或者進行URL重寫的時候儘可能優雅的處理意想不到的鏈接。例如,如果你有一個多頁的報告在 /報告下,使用了 pushState 分成了 /報告/1,/報告/2,/報告/3 等等這麼多頁,你就要保證伺服器端的代碼可以優雅的對 /報告/未定義 這樣的鏈接響應。
另外一個稍次一點的可選方案是在你的 pushState 地址中使用查詢字元串的鏈接片段,就像 /報告?頁碼=2 和 /報告?頁碼=3。這樣最終的鏈接看起來可能不太好看,不過最起碼它們不會導致404錯誤。