1、背景javascript
使用ajax,能夠實現不須要刷新整個頁面就能夠進行局部頁面的更新。這樣能夠開發交互性很強的富客戶端程序,減小網絡傳輸的內容。但長期以來存在一個問題,就是沒法利用瀏覽器自己提供的前進和後退按鈕進行操做。好比在頁面執行某個動做,該動做利用ajax請求到服務器獲取數據,更新了當前頁面的某些內容,這時想回到操做前的界面,用戶就會習慣點擊瀏覽器的後退按鈕,實際這裏是無效的(要麼頁面沒反應,要麼打開一個前面打開的過的頁面),或者想收藏當前頁面(以便於從新打開時直接顯示當前的信息),也是沒法作到的。css
這個問題由於html5的新特性而得以能夠解決。但不是直接解決了。而是提供了一些新的api,須要程序員編寫代碼來實現。下面咱們將詳細的來介紹。html
若是你對此問題和html5的這些新特性已經有些瞭解,能夠直接跳到最後的案例章節。html5
2、history對象分析java
瀏覽器是經過 window對象的 history對象來對瀏覽器歷史訪問記錄,從而能夠實現前進和後退。history對象能夠理解其保存了一個有序的列表對象,每一個對象都表明了一個頁面信息(包括頁面的url等信息),注意當前頁面也被保存在裏面。jquery
這樣就能夠經過瀏覽器自己提供的前進和後退按鈕來操做,也能夠利用javascript調用history對象的back(),forward(),和go()方法來實現頁面的切換。程序員
咱們先來理解下history的機制。history對象中記錄了瀏覽器窗口訪問過的url,但出於安全考慮,沒法經過程序獲取history對象中的具體信息,只能經過back、forward、go方法進行頁面跳轉,此外length屬性記錄了history中的記錄(url)條數。web
咱們設想下,當在瀏覽器窗口打開第一個地址,好比 url1時, 這時history中就有了url1這個記錄,且length屬性值爲1,history對象中有個當前頁面指針(從概念上能夠這麼理解)指向url1;若是再打開一個url2頁面(不管是經過在地址欄直接輸入、或經過url1中的連接或js代碼打開),這時history中就有了url1和url2這兩個記錄,是一個有序的列表,這時length屬性值爲2,history對象中的當前頁面指針指向url2,這時url2是最新的頁面,頁面不能夠前進,但能夠後退到url1,這時若是點擊瀏覽器自己提供的後退按鈕(或用js調用back方法),這時url1頁面會被從新加載顯示,history對象的length仍然爲2,url1和url2組成的列表仍然不變,但history對象中的當前頁面指針指向url1了,這時就不能後退但能夠前進了。能夠理解成一個數據結構中的雙向鏈表機制。ajax
經過上面的描述咱們能夠看出,咱們說的歷史記錄都是指一個完整的頁面請求url,而ajax並非一個完整的頁面請求,所以瀏覽器沒法記錄ajax的操做信息。chrome
3、history對象的新特性
HTML5引入了histtory.pushState()和history.replaceState()這兩個方法,它們會更新history對象的內容。同時,結合window.onpostate事件,就能夠解決文章開頭提出的問題。
咱們先來看pushState方法的含義,咱們經過舉例子的方式來更好的說明,先給出一段代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>測試</title> <script type="text/javascript" src="jquery.min.js"></script> </head> <body> <button onclick="doPushState()">pushState</button> <button onclick="count()">count</button> <script> var index=1; $(function(){ alert(location.href); }); function doPushState(){ history.pushState({}, "newtitle","test"+(index++)+".html"); } function count(){ alert(window.history.length); } </script> </body> </html>
上面是一個完整的html文件,文件名爲demo.html。 把該文件放到web服務器上,從瀏覽器訪問。若是是直接從本地磁盤打開,文件中的js代碼執行會報錯誤。
在一個新的瀏覽器窗口訪問該demo.html。 首先會執行 $()方法,彈出代碼中的location.href信息。 這時執行count按鈕,顯示爲1,注意若是在ie或chrome的新的瀏覽器窗口打開,值可能爲2,由於它們的窗口會加載系統默認的一個頁面,不是一個空白的窗口。
這時咱們每點擊一下pushState按鈕,發現瀏覽器的地址會發生變化,前後變爲test1.html , test2.html, test3.html, .......,而且經過點擊count按鈕發現,彈出的值加1. 這說明每調用一次pushState方法,history中就會新增長一條url記錄。
咱們先來解釋下pushState方法,該方法有三個參數:
1)第一個參數是個js對象,能夠聽任何的內容,能夠在onpostate事件中(後面介紹)獲取到便於作相應的處理。
2)第二個參數是個字符串,目前好像沒有起做用,能夠傳個空串。
3)第三個參數是個字符串,就是保存到history中的url。
結合例子的代碼和輸出能夠看出,調用pushState方法的做用,就是至關於打開一個新頁面,把當前頁面做爲歷史記錄,而當前的地址欄顯示的是pushState方法中的url(這裏是test.html)。可是與普通的打開一個新頁面不一樣。瀏覽器將不會在調用pushState()方法後加載這個url,也就是說即便你寫一個錯誤的url,也不會報錯。
能夠這麼理解,當咱們在一個新的瀏覽器窗口打開 demo.html後,點擊n次pushState按鈕後,history對象中存在這樣的一個ulr列表。
demo.html(第1個url)----> index1.html(第2個url).......->index?.html(第n個url)----->indexn.html(當前頁面的url)。
這時咱們須要點擊瀏覽器上的回退按鈕n次,才能將瀏覽器上的地址退回到 demo.html。並且不管是在點擊pushState按鈕 或點擊回退按鈕的過程當中發現,$()方法根本沒有被觸發,也就是說整個過程瀏覽器的頁面內容都沒有發生變化,變化的只是地址信息。
這也進一步說明,pushState只是將當前頁面保存到history的歷史記錄中(並做爲最近的一個記錄),而且將當前瀏覽器的地址欄改成參數url的指定的值,但並不會加載它。這點與普通的經過連接打開或瀏覽器地址輸入url徹底不同。
到了這裏咱們能夠想象一下文章開頭提出的問題了,若是咱們在頁面中執行一個ajax操做,當操做成功(如更新頁面的局部內容)後,咱們經過代碼調用pushState方法,設置一個新的url,這樣看上去就像發起了一個全新的請求,實際上只是個ajax操做。這時回退按鈕也能用了,問題僅僅這樣,回退按鈕點了也沒有任何反應。若是咱們能經過代碼,來響應這個回退按鈕觸發的事件,在事件中讓界面恢復到ajax請求以前的界面,問題不就解決了嗎?
得確如此,解決思路就是上面說的。下面咱們來經過一個實際的例子看如何實現。在介紹例子以前,咱們先來解釋下html5中 history新增的另外一個方法replaceState方法。
replaceState方法與pushState相似,一樣有三個參數。區別在於,replaceState()是用來修改history對象中記錄的當前頁面的信息,它不是新建一個記錄。若是將上面例子中的 代碼 history.pushState({}, "newtitle","test"+(index++)+".html"); 中的pushState改成replaceState,其它代碼都不動。這時咱們點擊pushState按鈕後,看到的現象是同樣的,地址欄的地址不斷變化,頁面內容不變。但咱們點擊count按鈕,發現history中的記錄數不變。這說明replaceState只是改變當前頁面在history對象中的記錄信息;而pushState是會產生一個新記錄做爲當前記錄,把當前頁面做爲歷史的記錄保存。
咱們再來看下window對象的popstate事件,當進行頁面的前進或回退時,會觸發該事件,而且在事件響應函數中經過 history.state 能夠獲取到 pushState方法和replaceState方法中第一個參數指定的對象。
解釋了這幾個api後,咱們來一個具體的例子。
4、具體案例
咱們來設想這樣一個應用。一個頁面來顯示一篇長文章,該文章內容很長,分爲不少章節。咱們但願頁面不會一次把全部章節的內容都加載起來,而是有一個章節導航,點擊每一個章節連接,經過jax加載具體章節的內容,而其它頁面部分不須要要變化。
咱們先看下傳統的實現代碼(注意,這裏只注意核心邏輯代碼的實現,其它的頁面佈局等儘可能簡化):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test2</title> <style type="text/css"> div { padding-bottom:100px; } </style> <script type="text/javascript" src="jquery.min.js"></script> </head> <body> <div style="float:left;border:1px solid red;margin:20px"> <p><a href="javascript:;" id="section1">第1章</a></p> <p><a href="javascript:;" id="section2">第2章</a></p> <p><a href="javascript:;" id="section3">第3章</a></p> </div> <div style="float:left;border:1px solid red;margin:20px" id="content"> </div> <script> $(function(){ //添加連接的處理事件 $("a").click(ajax); //加載默認的章節,默認顯示第1章 $("#section1").trigger("click"); }); function ajax(event){ //實際的流程是發起ajax請求,獲取內容並顯示。這裏爲了簡化,沒有寫實際的ajax請求。 //這段代碼應該在ajax的請求響應中編寫。 $("#content").html(this.id+"的內容"); var title = this.id; document.title = title; } </script> </body> </html>
在瀏覽器加載該頁面,當咱們點擊不一樣的章節連接時,內容會跟着變化,瀏覽器的標題也跟着變化。可是:
1)回退、前進按鈕用不了
2)當咱們刷新頁面時,無論當前在哪一個章節,都會從新回到第一個章節。
3)地址欄的url沒有變化,也意味着咱們無法把某個章節的地址保存下來,之後再次打開直接顯示該章節內容。
上面就是傳統ajax應用的一些弊端。下面咱們就來解決這些問題。
咱們先給出解決代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test2</title> <style type="text/css"> div { padding-bottom:100px; } </style> <script type="text/javascript" src="jquery.min.js"></script> </head> <body> <div style="float:left;border:1px solid red;margin:20px"> <p><a href="javascript:;" id="section1">第1章</a></p> <p><a href="javascript:;" id="section2">第2章</a></p> <p><a href="javascript:;" id="section3">第3章</a></p> </div> <div style="float:left;border:1px solid red;margin:20px" id="content"> </div> <script> $(function(){ //添加連接的處理事件 $("a").click(ajax); //加載默認的章節 changeContent(); //添加popstate事件 $(window).on("popstate",function(){ changeContent(); }); }); function changeContent(){ var query = location.href.split("?")[1]; if (!query) { // 若是沒有查詢條件,則顯示默認第1個章節 history.replaceState(null, "", location.href + "?name=" + $("#section1")[0].id); changeContent(); } else { //觸發按鈕click事件,加載內容, //注意不要漏了true參數,這樣能夠和用戶直接點擊觸發的頁面變化區別開來 $("#"+query.split("=")[1]).trigger("click",true); } } function ajax(event,isPopstate){ $("#content").html(this.id+"的內容"); var title = this.id; document.title = title; if(!isPopstate){ history.pushState(null, "", location.href.split("?")[0] + "?name=" + title); } } </script> </body> </html>
加載上面頁面,測試下,全部的問題都解決了。下面咱們來解釋下上述代碼。
咱們先看changeContent方法,該方法首先獲取頁面的url地址,判斷該地址是否有查詢條件(是否帶章節信息),若是沒有,認爲要顯示第一章節。咱們利用history的replaceState方法來改變當前的url,加上 name=section1的查詢條件,表示是第1章。由於replaceState方法不會改變頁面內容,所以還須要接着再調用changeContent方法。若是地址帶了查詢條件,認爲已經指定顯示某個章節內容,這時觸發章節連接的click事件。
咱們再看ajax方法,就是章節連接的click事件響應函數,爲了簡化,該函數沒有發起實際的ajax請求,而是至關於直接處理ajax返回的結果。首先是用獲得的結果更新頁面(這裏是直接寫死的),而後更新標題,這與傳統的ajax作法同樣。關鍵的區別是,判斷該方法若是是用戶點擊的(不是onpopstate事件處理的),就會調用history對象的pushState方法來將當前頁面信息保存到history對象中,並新增一個記錄信息表明ajax請求後的頁面。
changeContent方法一樣是onpopstate事件的處理函數,其功能就是利用獲取到的url信息(保存在history記錄)中,來經過ajax獲取到對應的內容,讓頁面顯示相應的信息。 從用戶感知上看,就跟正常的回退、前進致使的頁面切換同樣。用戶感受不到是ajax請求,還覺得就是多個獨立的頁面在切換。
5、小結
本文詳細的介紹瞭如何利用html5的新特性來解決傳統ajax請求致使的一些缺陷。經過上面的介紹能夠看出,爲了解決問題,仍是須要程序員作很多的事情,對於一個實際的項目來講,最好能在框架層面進行封裝解決,而不是要讓每一個具體頁面的實現者都來處理。這個能夠是下一步要考慮的內容。