HTML5新特性之History
幾年前,Ajax的興起給互聯網帶來了新的生機,同時也使用戶體驗有了質的飛躍,用戶無需刷新頁面即可獲取新的數據,而頁面也以一種更具有交互性的形式為用戶展現視圖,可以說這種變化對互聯網發展的貢獻是史無前例的。
但隨著Ajax大規模應用,越來越多的開發人員開始註意到其中存在的問題,因為Ajax的視圖展現是在頁面無刷新情況下進行的,這也就意味著在用戶做了一系列操作之後,頁面的URL是沒有任何變化的,這些操作所產生的結果自然也就無法保留,當用戶再次訪問時,想要展現的數據也就無法重現。
舉個例如來說,我們在一個攝影網站瀏覽一個相冊,點擊第一張圖片會動態彈出一個大圖的相框,可以更好地瀏覽圖片,當看到一個自己喜歡的圖片時,急於分享這張圖片,於是把當前的URL發布出去了,但是我們的好友點擊進去後發現,看到的只是原來的那個相冊,並沒有彈出的大圖相框,更別說我們要分享的那張圖片了,是不是很失望。也正是因為如此,這個頁面無法被搜索引擎精確地抓取,SEO無法做到優化,用戶的可訪問性也大打折扣。
為了解決這個問題,開發者開始嘗試使用URL中的hash來提高可訪問性(hash 屬性是一個可讀可寫的字符串,該字符串是 URL 的錨部分(從 # 號開始的部分)),部分瀏覽器開始提供hash相關的事件支持,有些知名站點也和搜索引擎約定使用某種規則來對站點頁面進行抓取,但這畢竟不是一個標準的技術,還是不可避免的存在很多問題,開發者也期待出現一個標準化的完美解決方案,徹底解決這個難題。
幸運的是,HTML5中的History API給開發者帶了新的希望,它很好的支持了基於URL的頁面無刷新操作,也使得SEO優化得到完美的解決,毫無疑問,History API將會成為Web領域未來的標準,越來越多的開發者也將會使用它來開發自己的應用程序。
那麽今天,我們就來講解一下與History相關的技術。
為了能夠更好的闡述這項新技術,我們選擇使用一個示例程序來開始。如下圖所示,我們展示三張小圖,點擊任意一張,將會彈出大的預覽圖,然後可以前後切換預覽圖片:
這個程序對於大家來說應該說是比較簡單的,當點擊小圖時,將放置大圖的頁面元素顯示出來,圖片和簡介顯示出與小圖對應的信息即可,切換預覽圖片的操作也是非常容易做到的。
這就是傳統的Ajax操作,而這種操作存在一個極其嚴重的缺陷,我們無法記錄當前瀏覽的圖片信息,假如後面的小圖有幾百張,當我們瀏覽到某一張時,覺得非常不錯,想把它發送給朋友,也有可能想自己存下來,這時候我們會發現,URL中沒有任何這張圖片的信息,復制地址欄也就沒有任何效果了。
所以我們極為迫切的需要在URL中加入用戶操作所產生的信息,這個時候History API就派上用場了,他提供的方法可以在頁面不刷新的情況下對URL進行更新操作。下面就介紹一下主要的兩個方法:
history.pushState(stateObj, title, url);
這個方法會往當前會話的歷史棧中放入一條記錄,stateObj是用戶自定義的對象,用於記錄一些有用的數據,title顧名思義就是標題信息,第三個參數是要放入的url信息。例如我們點擊第二張圖片時可以執行history.pushState({id: 2}, ‘img: 2‘, ‘?preview=2‘);
history.replaceState(stateObj, title, url);
這個方法於pushState類似,唯一不同的是,執行這個操作後,瀏覽器會對會話歷史棧內的記錄進行替換而不是添加,這在特定場景下是比較適用的。
我們這裏就來結合實例來講解一下如何操作歷史記錄。
正如上圖看到的那樣,頁面初始化時展現三張小圖,點擊任意一張小圖時,會彈出預覽圖,頁面的基本框架如下:
<!DOCTYPE html> <html> <head> <title>History</title> <link rel="stylesheet" type="text/css" href="css/main.css"> </head> <body> <ul id="gallery"> <li> <img src="img/1.jpg" data-id="1"/> </li> <li> <img src="img/2.jpg" data-id="2"/> </li> <li> <img src="img/3.jpg" data-id="3"/> </li> </ul> <div id="shadow-layer" class="for-dialog"></div> <div id="preview-panel" class="for-dialog"> <img id="preview-img"/> <div id="preview-info"></div> <div id="preview-close" class="preview-button">Close</div> <div id="preview-prev" class="preview-button"><</div> <div id="preview-next" class="preview-button">></div> </div> </body> <script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/main.js"></script> </html>
然後頁面初始化時,我們需要為一些DOM元素綁定事件,就像下面這樣:
//點擊小圖開始預覽模式 $(‘#gallery img‘).click(function() { preview(parseInt($(this).attr(‘data-id‘))); }); //關閉預覽視圖 $(‘#preview-close‘).click(function() { $(‘.for-dialog‘).hide(); window.history.back(); }); //點擊預覽上一張 $(‘#preview-prev‘).click(function() { switchPrev(); }); //點擊預覽下一張 $(‘#preview-next‘).click(function() { switchNext(); });
可以看到幾個綁定的事件,點擊小圖會觸發預覽模式;然後在預覽時點擊關閉按鈕,會隱藏預覽框,同時調用了history的back函數,我們稍後會介紹;最後是兩個切換按鈕的事件,分別會切換到上一張或下一張大圖。我們註意到,當小圖被點擊時,調用了一個叫preview的函數,這個函數正是我們重點要講的,代碼如下:
function preview(id, isSwitch) { $(‘.for-dialog‘).show(); //獲取對應的數據信息 var data = getPreviewDataById(id); $(‘#preview-img‘).attr(‘src‘, data.img); $(‘#preview-info‘).html(data.info); //記錄當前預覽圖片的ID,在前後切換時取用 $(‘#preview-panel‘).attr(‘data-prview-id‘, id); var stateObj = {id: id}; var operation = isSwitch ? ‘replaceState‘ : ‘pushState‘; //往歷史記錄棧中放入一條記錄或者替換一條記錄 window.history[operation](stateObj, ‘img: ‘ + id, ‘?preview=‘ + id); }
上面的preview函數主要負責幾個重要的工作,首先顯示預覽框,然後根據數據ID獲取數據信息,最後操作歷史記錄對象。在實際開發中,getPreviewDataById函數大多是以Ajax方式進行的,這個時候只需將後面的邏輯放到回調函數內部即可。在preview函數的最後,我們往歷史記錄裏面放入或替換一條包含ID信息的對象,同時改變URL,放入還是替換,取決於是不是前後切換預覽圖,我們稍後會詳細解釋一下。註意,這個時候雖然URL改變了,但頁面並不會刷新。下面我們就來對比一下點擊前後的效果,以點擊第二張小圖為例:
可以明顯看到,地址欄已經變化了,這其實就是我們調用pushState函數的結果:
window.history.pushState({id: 2}, ‘img: 2‘, ‘?preview=2‘);
需要額外註意的一點是,第三個參數是新的URL地址,根據官方文檔介紹,它可以是相對地址,也可以是絕對地址,所以我們也可以選擇使用絕對地址,就像下面這樣,效果是一樣的:
window.history.pushState({id: 2}, ‘img: 2‘,‘http://localhost/history/?preview=2‘);
在上面的preview函數參數列表中,存在一個isSwitch參數,如果函數被調用時指定這個參數,則會調用replaceState,而不是pushState,這是為什麽呢,下面就來解釋一下。
還記得我們我們為預覽框右上角關閉按鈕綁定的事件嗎,除了隱藏預覽圖,還調用了history的back函數,其實我們是要利用這個函數從歷史棧中彈出之前放入的歷史記錄,進而將URL恢復到預覽前的狀態。那麽,如果我們在預覽的時候頻繁的切換到下一張或者上一張,歷史記錄棧中就會存在很多記錄,要回到初始狀態顯然不易,我們也沒有必要保留這麽多的歷史記錄,所以當用戶前後切換預覽圖時,只需要調用replaceState替換當前歷史棧中那條記錄即可,假如我們點擊右邊的切換按鈕,應該是這樣的:
window.history.replaceState({id: 3}, ‘img: 3‘, ‘?preview=3‘);
為了調用replaceState函數,我們需要在執行preview函數時,傳入一個isSwitch參數,我們來看看是如何調用的:
//預覽下一張 function switchNext() { //根據當前ID計算下一個ID var currId = parseInt($(‘#preview-panel‘).attr(‘data-prview-id‘)); currId++; currId > 3 && (currId = 1); preview(currId, true); }
下面就以兩個示意圖來講解一下切換前後的棧結構:
這樣我們就能保證歷史棧中只有一條記錄,當關閉預覽框結束預覽時,可立即恢復到之前的視圖。
而向前切換也是一樣的,代碼如下:
//預覽上一張 function switchPrev() { //根據當前ID計算前一個ID var currId = parseInt($(‘#preview-panel‘).attr(‘data-prview-id‘)); currId--; currId < 1 && (currId = 3); preview(currId, true); }
到這裏,我想大家都已經清楚如何操作歷史棧來改變URL,那麽我們不僅要問,如果使用當前的URL重新訪問站點,如何還原關閉之前的視圖呢,其實這個就屬於基本的操作了,只需要在onload函數時得到相應的參數,然後調用preview函數即可,如代碼所示:
//從URL中獲取到當前預覽圖片的ID function getCurrPreviewId() { var search = location.search; if (!search) return -1; var params = {}; var keyValues = search.substring(1).split(‘&‘); keyValues.forEach(function(item) { var parts = item.split(‘=‘); params[parts[0]] = parts[1]; }); if (!params[‘preview‘]) return -1; return params[‘preview‘]; } //當頁面加載時,如果URL中有預覽信息,取出然後 window.onload = function() { var id = getCurrPreviewId(); if (id < 1 || id > 3) return; preview(id); };
到這裏其實我們已經將整個過程講解完成了,但還有一個重要的事件我們還沒有涉及到,那就是window下面的onpopstate事件,當用戶點擊後退按鈕或前進按鈕時,或者當我們的程序執行history.back(),history.forward(),history.go()時,都會觸發這個事件,它能夠捕獲這些操作完成之後,當前歷史棧中的狀態,我們可以利用這些信息,做進一步的操作。那麽我們就來做個試驗,看看onpopstate事件是如何被觸發的:
window.onpopstate = function(e) { console.log(‘e.state:‘, e.state); };
在這個事件函數內,我們會打印事件對象中的state對象,其實就是我們執行pushState函數時放進去的對象,下面我們在控制臺做下面幾個操作看看效果:
如圖所示,我們放入兩條記錄,當後退或前進的時候,都會執行到onpopstate事件函數,並且能夠獲取到當前的狀態對象信息,如果我們的圖片預覽功能需要記錄每一步的操作,那麽我們就可以在onpopstate事件函數中獲取到當前需要展現的圖片ID,然後直接調用preview函數即可。
最後,需要註意的是,當前有些瀏覽器還不能很好的支持History API,所以我們最好判斷一下瀏覽器的支持情況:
var isHistoryStateSupported = ‘pushState‘ in window.history;
講到這裏,關於History的相關知識及應用基本上就告一段落了,希望大家能夠細細體會它的應用場景,並加以練習,最後希望大家能夠很好地掌握並運用到未來的項目中去。
HTML5新特性之History