前端路由詳解

什麼是前端路由?

路由是根據不同的 url 地址展示不同的內容或頁面,就是把不同路由對應不同的內容或頁面的任務交給前端來做,之前是通過服務端根據 url 的不同返回不同的頁面實現的。

什麼時候使用前端路由?

在單頁面應用中,大部分頁面結構不變,只改變部分內容的使用。

前端路由有什麼優點和缺點?

優點:用戶體驗好,不需要每次都從服務器全部獲取,快速展現給用戶

缺點:使用瀏覽器的前進,後退鍵的時候會重新發送請求,沒有合理地利用緩存,單頁面無法記住之前滾動的位置,無法在前進,後退的時候記住滾動的位置。

實現的幾種方式和幾種場景

故事從名叫 Oliver 的綠箭蝦說起,這位大蝦酷愛社交網站,一天他打開了 Twitter ,從發過的 tweets 的選項卡一路切到 followers 選項卡,Oliver發現頁面的內容變化了,URL也變化了,但爲什麼頁面沒有閃爍刷新呢?於是Oliver打開的網絡監控器,他驚訝地發現在切換選項卡時,只有幾個XHR請求發生,但頁面的URL卻在對應着變化,這讓 Oliver 不得不去思考這一機制的原因…

敘事體故事講完,進入正題。首先,我們知道傳統而經典的 Web 開發中,服務器端承擔了大部分業務邏輯,但隨着 2.0 時代 ajax 的到來,前端開始擔負起更多的數據通信和與之對應的邏輯。

在過去,Server 端處理來自瀏覽器的請求時,要根據不同的 Url 路由,拼接出對應的視圖頁面,通過 Http 返回給瀏覽器進行解析渲染。Server 不得不承擔這份艱鉅的責任。爲了讓 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 的官方文檔描述,我們先來關注下最後邊的兩個apihistory.pushStatehistory.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 都會更新或者覆蓋當前瀏覽器的 titleurl 爲對應傳入的參數。

url 參數可以爲絕對路徑,如: http://tonylee.pw?name=tonyleehttps://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 作爲一個改變當前瀏覽器地址的參數,用法是很靈活的,replaceStatepushState 具有和上邊測試相同的特性,傳入的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 有一個應用 pushStatereplaceStatedemo 大家可以看一下:

<!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 內核瀏覽器纔會默認觸發 popstatechrome>34 的可能實現的有問題, safari 就很正常)。

到這裏,說了這麼多 api, 其實我們對標籤化/路由系統應該有了一個大概的瞭解。如果考慮H5的api,過去 facebooktwitter 實現路由系統時,約定用”#!”實現,這估計也是一個爲了照顧搜索引擎的約定。畢竟前端路由系統涉及到大量的 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標籤化的問題。以上說了這麼多,僅僅是分析了這些路由系統大概的實現方式和兼容性解決方案。

參考文章: 什麼是“前端路由”?什麼時候適合使用“前端路由”?“前端路由”有哪些優點和缺點?& Web開發中 前端路由 實現的幾種方式和適用場景

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章