故事從名叫Oliver的綠箭蝦`提及,這位大蝦酷愛社交網站,一天他打開了 Twitter ,從發過的tweets的選項卡一路切到followers選項卡,Oliver發現頁面的內容變化了,URL也變化了,但爲何頁面沒有閃爍刷新呢?因而Oliver打開的網絡監控器(沒錯,Oliver是個程序員),他驚訝地發如今切換選項卡時,只有幾個XHR請求發生,但頁面的URL卻在對應着變化,這讓Oliver不得不去思考這一機制的緣由… 敘事體故事講完,進入正題。首先,咱們知道傳統而經典的Web開發中,服務器端承擔了大部分業務邏輯,但隨着2.0時代ajax的到來,前端開始擔負起更多的數據通訊和與之對應的邏輯。 在過去,Server端處理來自瀏覽器的請求時,要根據不一樣的Url路由,拼接出對應的視圖頁面,經過Http返回給瀏覽器進行解析渲染。Server不得不承擔這份艱鉅的責任,誰叫他是Server,而不是Owner -_-「。爲了讓Server端更好地把重心放到實現核心邏輯和看守數據寶庫,把部分數據交互的邏輯交給前端擔負,讓前端來分擔Server端的壓力顯得尤其重要,前端也有這個責任和能力。 那麼問題來了,前端的能力是什麼呢,有哪些能力呢? 大部分的複雜的網站,都會把業務解耦爲模塊進行處理。這些網站中又有不少的網站會把適合的部分應用Ajax進行數據交互,展示給用戶,很明顯處理這樣的數據通訊交互,不可避免的會涉及到跟URL打交道,讓數據交互的變化反映到URL的變化上,進而能夠給用戶機會去經過保存的URL連接,還原剛纔的頁面內容板塊的佈局,這其中包括Ajax局部刷新的變化。 經過記錄URL來記錄web頁面板塊上Ajax的變化,咱們能夠稱之爲 Ajax標籤化 ,比較好實現能夠參考 Pjax 等。而對於較大的framework,咱們稱之爲 路由系統 ,好比AngularJs等。 咱們先熟悉幾個新的H5 history Api: /*Returns the number of entries in the joint session history.*/ window . history . length /*Returns the current state object.*/ window . history . state /*Goes back or forward the specified number of steps in the joint session history.A zero delta will reload the current page.If the delta is out of range, does nothing.*/ window . history . go( [ delta ] ) /*Goes back one step in the joint session history.If there is no previous page, does nothing.*/ window . history . back() /*Goes forward one step in the joint session history.If there is no next page, does nothing.*/ window . history . forward() /*Pushes the given data onto the session history, with the given title, and, if provided and not null, the given URL.*/ window . history . pushState(data, title [url] ) /*Updates the current entry in the session history to have the given data, title, and,if provided and not null, URL.*/ window . history . replaceState(data, title [url] ) 上邊是Mozilla在HTML5中實現的幾個History api的官方文檔描述,咱們先來關注下最後邊的兩個api, history.pushState 和 history.replaceState ,這兩個history新增的api,爲前端操控瀏覽器歷史棧提供了可能性: /** *parameters *@data {object} state對象,這是一個javascript對象,通常是JSON格式的對象 *字面量。 *@title {string} 能夠理解爲document.title,在這裏是做爲新頁面傳入參數的。 *@url {string} 增長或改變的記錄,對應的url,能夠是相對路徑或者絕對路徑, *url的具體格式能夠自定。 */ history.pushState(data, title, url) //向瀏覽器歷史棧中增長一條記錄。 history.replaceState(data, title, url) //替換歷史棧中的當前記錄。 這兩個Api都會操做瀏覽器的歷史棧,而不會引發頁面的刷新。不一樣的是,pushState會增長一條新的歷史記錄,而replaceState則會替換當前的歷史記錄。所需的參數相同,在將新的歷史記錄存入棧後,會把傳入的data(即state對象)同時存入,以便之後調用。同時,這倆api都會更新或者覆蓋當前瀏覽器的title和url爲對應傳入的參數。 url參數能夠爲絕對路徑,如: http://tonylee.pw?name=tonylee ,https://www.tonylee.pw/name/tonylee ;也能夠爲相對路徑: ?name=tonylee , /name/tonylee ;等等的形式,讓咱們來在console中作個測試: //假設當前網頁URL爲:http://tonylee.pw window.history.pushState(null, null, "http://tonylee.pw?name=tonylee"); //url變化:http://tonylee.pw -> http://tonylee.pw?name=tonylee window.history.pushState(null, null, "http://tonylee.pw/name/tonylee"); //url變化:http://tonylee.pw -> http://tonylee.pw/name/tonylee window.history.pushState(null, null, "?name=tonylee"); //url變化:http://tonylee.pw -> http://tonylee.pw?name=tonylee window.history.pushState(null, null, "name=tonylee"); //url變化:http://tonylee.pw -> http://tonylee.pw/name=tonylee window.history.pushState(null, null, "/name/tonylee"); //url變化:http://tonylee.pw -> http://tonylee.pw/name/tonylee window.history.pushState(null, null, "name/tonylee"); //url變化:http://tonylee.pw -> http://tonylee.pw/name/tonylee //錯誤的用法: window.history.pushState(null, null, "http://www.tonylee.pw?name=tonylee"); //error: 因爲跨域將產生錯誤 能夠看到,url做爲一個改變當前瀏覽器地址的參數,用法是很靈活的,replaceState和pushState具備和上邊測試相同的特性,傳入的url若是可能,總會被作適當的處理,這種處理默以」/」相隔,也能夠本身指定爲」?」等。要注意,這兩個api都是不能跨域的!好比在 http://tonylee.pw 下,只能在同域下進行調用,如二級域名http://www.tonylee.pw 就會產生錯誤。沒錯,我想你已經猜到了前邊講到的Oliver看到URL變化,頁面板塊變化,頁面發出XHR請求,頁面沒有reload等等特性,都是所以而生! 若是有興趣,你也能夠去twitter親自體驗twitter的這一特性,看看他的前端路由系統是如何工做的。 https://twitter.com/following -> https://twitter.com/followers 至於api中的data參數,其實是一個state對象,也便是javascript對象。Firefox的實現中,它們是存在用戶的本地硬盤上的,最大支持到640k,若是不夠用,按照FF的說法你能夠用 sessionStorage or localStorage -_-「。如: var stateObj = { foo: "bar" }; history.pushState(stateObj, "the blog of Tony Lee", "name = Later"); 若是當前頁面通過這樣的過程,歷史棧對應的條目,被存入了stateObj,那麼咱們能夠隨時主動地取出它,若是頁面只是一個普通的歷史記錄,那麼這個state就是null。如: var currentState = history.state; //若是沒有則爲null。 mozilla有一個應用pushState和replaceState小demo你們能夠看一下: <!DOCTYPE HTML> <!-- this starts off as http://example.com/line?x=5 --> <title>Line Game - 5</title> <p>You are at coordinate <span id="coord">5</span> on the line.</p> <p> <a href="?x=6" onclick="go(1); return false;">Advance to 6</a> or <a href="?x=4" onclick="go(-1); return false;">retreat to 4</a>? </p> <script> var currentPage = 5; // prefilled by server!!!! function go(d) { setupPage(currentPage + d); history.pushState(currentPage, document.title, '?x=' + currentPage); } onpopstate = function(event) { setupPage(event.state); } function setupPage(page) { currentPage = page; document.title = 'Line Game - ' + currentPage; document.getElementById('coord').textContent = currentPage; document.links[0].href = '?x=' + (currentPage+1); document.links[0].textContent = 'Advance to ' + (currentPage+1); document.links[1].href = '?x=' + (currentPage-1); document.links[1].textContent = 'retreat to ' + (currentPage-1); } </script> 仔細閱讀就會看到,這個demo已經快成爲一個Ajax標籤化或者前端路由系統的雛形了! 瞭解這倆api還不夠,再來看下上邊的demo中涉及到的 popstate 事件,我擔憂解釋的不到位,因此看看mozilla官方文檔的解釋: An event handler for the popstate event on the window. A popstate event is dispatched to the window every time the active history entry changes between two history entries for the same document. If the history entry being activated was created by a call to history.pushState() or was affected by a call to history.replaceState(), the popstateevent's state property contains a copy of the history entry's state object. Note that just calling history.pushState() or history.replaceState() won't trigger apopstate event. The popstate event is only triggered by doing a browser action such as clicking on the back button (or calling history.back() in JavaScript). And the event is only triggered when the user navigates between two history entries for the same document. Browsers tend to handle the popstate event differently on page load. Chrome (prior to v34) and Safari always emit a popstate event on page load, but Firefox doesn't. Syntax window.onpopstate = funcRef; //funcRef is a handler function. 簡而言之,就是說當同一個頁面在歷史記錄間切換時,就會產生popstate事件。正常狀況下,若是用戶點擊後退按鈕或者開發者調用:history.back() or history.go(),頁面根本就沒有處理事件的機會,由於這些操做會使得頁面reload。因此popstate只在不會讓瀏覽器頁面刷新的歷史記錄之間切換才能觸發,這些歷史記錄通常由pushState/replaceState或者是由hash錨點等操做產生。而且在事件的句柄中能夠訪問state對象的引用副本!並且單純的調用pushState/replaceState並不會觸發popstate事件。頁面初次加載時,知否會主動觸發popstate事件,不一樣的瀏覽器實現也不同。下邊是官方的一個demo: window.onpopstate = function(event) { alert("location: " + document.location + ", state: " + JSON.stringify(event.state)); }; history.pushState({page: 1}, "title 1", "?page=1"); history.pushState({page: 2}, "title 2", "?page=2"); history.replaceState({page: 3}, "title 3", "?page=3"); history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}" history.back(); // alerts "location: http://example.com/example.html, state: null history.go(2); // alerts "location: http://example.com/example.html?page=3, state: {"page":3} 這裏即是經過event.state拿到的state的引用副本! H5還新增了一個 hashchange 事件,也是頗有用途的一個新事件: The 'hashchange' event is fired when the fragment identifier of the URL has changed (the part of the URL that follows the # symbol, including the # symbol). 當頁面hash(#)變化時,即會觸發hashchange。錨點Hash起到引導瀏覽器將此次記錄推入歷史記錄棧頂的做用, window.location 對象處理「#」的改變並不會從新加載頁面,而是將之當成新頁面,放入歷史棧裏。而且,當前進或者後退或者觸發hashchange事件時,咱們能夠在對應的事件處理函數中註冊ajax等操做! 可是hashchange這個事件不是每一個瀏覽器都有,低級瀏覽器須要用輪詢檢測URL是否在變化,來檢測錨點的變化。當錨點內容(location.hash)被操做時,若是錨點內容發生改變瀏覽器纔會將其放入歷史棧中,若是錨點內容沒發生變化,歷史棧並不會增長,而且也不會觸發hashchange事件。 想必你猜到了,這裏說的低級瀏覽器,指的就是可愛的IE了。好比我有一個url從http://tonylee.pw#hash_start=1 變化到http://tonylee.pw#hash_start=2 ,實現良好的瀏覽器是會觸發一個名爲hashchange 的事件,可是對於低版本的IE(稍後我會對具體的兼容性作個總結),咱們只能經過設置一個Inerval來不斷的輪詢url是否發生變化,來判斷是否發生了相似hashchange的事件,同時能夠聲明對應的事件處理函數,從而模擬事件的處理。以下是當瀏覽器不支持hashchange事件時的模擬方法: (function(window) { // 若是瀏覽器不支持原生實現的事件,則開始模擬,不然退出。 if ( "onhashchange" in window.document.body ) { return; } var location = window.location, oldURL = location.href, oldHash = location.hash; // 每隔100ms檢查hash是否發生變化 setInterval(function() { var newURL = location.href, newHash = location.hash; // hash發生變化且全局註冊有onhashchange方法(這個名字是爲了和模擬的事件名保持統一); if ( newHash != oldHash && typeof window.onhashchange === "function" ) { // 執行方法 window.onhashchange({ type: "hashchange", oldURL: oldURL, newURL: newURL }); oldURL = newURL; oldHash = newHash; } }, 100); })(window); 熟悉了這些新的H5 api,大概對前端路由的實現方式,有了一個小小的模型了。咱們來看下兼容性: <script type="text/javascript" src="./jquery-1.9.1.js"></script> <script> $(function (){ if(history&&history.pushState){ alert("true"); }else{ alert("false"); } $(window).on("hashchange",function (){ alert("hashchange"); }); }); </script> 由上邊的測試我得出了一些兼容性概覽: history&&history.pushState兼容以下: chrome true; Firefox true; IE10 true; IE<=9 false; PS:ie<=9既然不支持這些api那就只能採用hash方案,來實現路由系統的兼容了。 hashchange兼容以下: IE9 true; IE8 true; IE7 false; ... 頁面load時,onhashchange默認觸發狀況: chrome 需主動trigger才能觸發 FF 需主動trigger才能觸發 IE 需主動trigger才能觸發 頁面load時,onpopstate默認觸發狀況: chrome <34版本以前的默認觸發 FF 默認不觸發 IE 默認不觸發 PS:以上是我手動測試的一個大概狀況,具體的兼容狀況能夠去這裏測試(http://caniuse.com/)。 只有webkit內核瀏覽器纔會默認觸發 popstate (chrome>34的可能實現的有問題,safari就很正常)。 到這裏,說了這麼多api, 其實咱們對標籤化/路由系統應該有了一個大概的瞭解。若是考慮H5的api,過去facebook和twitter實現路由系統時,約定用」#!」實現,這估計也是一個爲了照顧搜索引擎的約定。畢竟前端路由系統涉及到大量的ajx,而這些ajax對應url路徑對於搜索引擎來講,是很難匹配起來的。 路由大概的實現過程能夠這麼理解, 對於高級瀏覽器,利用H5的新Api作好頁面上不一樣板塊ajax等操做與url的映射關係,甚至能夠本身用javascript書寫一套歷史棧管理模塊,從而繞過瀏覽器本身的歷史棧。而當用戶的操做觸發popstate時,能夠判斷此時的url與板塊的映射關係,從而加載對應的ajax板塊。這樣你就能夠把一個具備很複雜ajax版面結構頁面的url發送給你的朋友了,而你的朋友在瀏覽器中打開這個連接時,前端路由系統url和板塊映射關係會解析並還原出整個頁面的原貌!通常SPA(單頁面應用)和一些複雜的社交站應用,會廣泛擁有本身的前端路由系統。 看到這裏,想必你也想到一個問題,瀏覽器第一次打開某個連接時,確定會首先被定向到server端進行路由解析,上邊所說的前端路由系統,都是創建在頁面已經打開,而且前端能夠利用H5等的api攔截下這些URL變化,確保這些URL變化不會發送的server端返回新的頁面。可是考慮這種狀況,連接是在一個新的瀏覽器tab中打開的,那麼這時候前端就沒法攔截下這個url,因此,這就要求serer和前端制定好一個規則,那些url是須要前端解析的,那些url是屬於後端的,而server判斷出這個url的某部分結構不是本身應該解決的部分時,它就應該意識到,這是前端路由系統的URL部分,須要定向到擁有前端路由系統javascript代碼的頁面,交給前端處理,好比,nodejs中: //Express框架的路由訪問控制文件server.js,增長路由配置。 app.use(function (req, res) { if(req.path.indexOf('/routeForServerSide')>=0){ res.send("這裏返回的都是server端處理的路由"); } //好比AngularJS頁面 else{ res.sendfile('這裏能夠將已經配置好angularJS路由的頁面返回'); } }); 經過這樣的方式,屬於前端的路由系統始終能夠被正確的交給前端路由系統去handle。對於php,.net也都是相似的配置server路由,給前端路由留下出口便可。 AngularJS框架中路由通常都這樣配置: app.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { $routeProvider .when('/login', { templateUrl: '/login.html', controller: 'LoginController' }).otherwise({ redirectTo: '/homepage' }); $locationProvider.html5Mode(true); }]) 能夠看到,angular正是將URL、模塊模板、模塊控制器,進行一個系統的映射,從而實現出一套前端路由系統。這套路由系統默認是以#號開始的,url中錨點#號後邊的url即標誌着前端路由系統URL部分的開始。這麼作是爲了照顧到更多瀏覽器,由於利用hash方案,IE對這套路由系統也會有很好的支持性(前邊已經說到,低版本IE對H5的新Api支持很差)。而若是項目壓根就不想考慮IE,在Ng中,就能夠直接調用$locationProvider.html5Mode(true) 來利用H5的api實現路由系統,從而去掉#號,不用hash方案,這樣作URL可能會更美觀一些-_-「。 正常狀況下,URL中的」/」通常是server端路由採用的標記,而」?」或者」#」再或者」#!」,則通常爲前端路由採用的開始標記,咱們能夠在這些符號後邊,經過鍵值對的形式,描述一個頁面具備哪些板塊配置信息。也不乏有的網站爲了美觀,先後端共用」/」進行路由索引(好比前邊說的twitter)。 咱們來看兩個比較經典的網站: 1.Sina(新浪) 做爲國內SNS的翹楚,新浪的路由形式也很高大上,好比: 在FF,Chrome,IE>=10時新浪的URL是這樣的: http://weibo.com/mygroups?gid=221102230086340215&wvr=5&leftnav=1 PS:能夠看到從?號開始就是前端路由了,一大堆的鍵值對。 在IE<=9時: http://weibo.com/mygroups?gid=221102230086340215&wvr=5&leftnav=1#!/mygroups?gid=221102230086340215&wvr=5&leftnav=1 PS:仔細觀察你會發現,新浪在#!後邊把路由段,複製了一遍,這是由於IE低版本不支持H5的新api,所以採用#號的hash方案(好比前邊講到的hashchange或輪詢等技術),這樣就照顧到全部的瀏覽器啦~ 2.Gmail 做爲一款超好用的SPA應用典範中的典範,不管從界面風格仍是易用性...好吧不扯了直接說路由: 收件箱:https://mail.google.com/mail/u/1/#inbox 星標箱:https://mail.google.com/mail/u/1/#starred 發件箱:https://mail.google.com/mail/u/1/#sent 草稿箱:https://mail.google.com/mail/u/1/#drafts PS:看到了麼,Gmail表示url不是給正常人看的,一概用#來實現前端路由部分,甚是簡潔明瞭(其實挺讚的!)。最重要的是,這種路由方案,兼容性沒的說(多是Gmail很看重IE用戶羣體)! 最後總結下: H5+hash方案:兼容因此瀏覽器,又照顧到了高級瀏覽器應用新特性。 純H5方案:表示IE是誰,我不認識-_-",這套方案應用純H5的新特性,URL隨心定製。 純Hash方案:其實一開始我是拒絕的,但是...但是...duang...IE~~:) 不論哪一種方案,最終的目的都是但願能解決ajax標籤化的問題。以上說了這麼多,僅僅是分析了這些路由系統大概的實現方式和兼容性解決方案,若是有機會,我會再寫一篇文章介紹下主流框架中或者類庫中,具體是如何實現這套路由系統的,javascript版本的歷史棧管理模塊又是怎麼樣的,實現思路如何。