一、Pjax
像Github、百度、微博等這些大站,已經不再使用普通的a標簽做跳轉了。他們大多使用Ajax請求替代了a標簽的默認跳轉,然后使用HTML5的新API修改了Url,你可以在F12的Network面板里發現這個秘密。
這項技術並沒有特別標准的學名,大家都稱呼為Pjax,意為PushState + Ajax。這並不完全准確,因為還有Hash + Ajax等方法,但為了方便,我們下文還是統稱為Pjax。
為什么要這么做?
Pjax是一個優秀的解決方案,你有足夠多的理由來使用它:
- 可以在頁面切換間平滑過渡,增加Loading動畫。
- 可以在各個頁面間傳遞數據,不依賴URL。
- 可以選擇性的保留狀態,如音樂網站,切換頁面時不會停止播放歌曲。
- 所有的標簽都可以用來跳轉,不僅僅是a標簽。
- 避免了公共JS的反復執行,如無需在各個頁面打開時都判斷是否登錄過等等。
- 減少了請求體積,節省流量,加快頁面響應速度。
- 平滑降級到低版本瀏覽器上,對SEO也不會有影響。
原理呢?
Pjax的原理十分簡單。 1. 攔截a標簽的默認跳轉動作。 2. 使用Ajax請求新頁面。 3. 將返回的Html替換到頁面中。 4. 使用HTML5的History API或者Url的Hash修改Url。
HTML5 History API
我們來看看HTML5在History里增加了什么:
history.pushState(state, title, url)
pushState方法會將當前的url添加到歷史記錄中,然后修改當前url為新url。請注意,這個方法只會修改地址欄的Url顯示,但並不會發出任何請求。我們正是基於此特性來實現Pjax。它有3個參數:
- state: 可以放任意你想放的數據,它將附加到新url上,作為該頁面信息的一個補充。
- title: 顧名思義,就是document.title。不過這個參數目前並無作用,瀏覽器目前會選擇忽略它,傳null即可。
- url: 新url,也就是你要顯示在地址欄上的url。
history.replaceState(state, title, url)
replaceState方法與pushState大同小異,區別只在於pushState會將當前url添加到歷史記錄,之后再修改url,而replaceState只是修改url,不添加歷史記錄。
window.onpopstate 事件 一般來說,每當url變動時,popstate事件都會被觸發。但若是調用pushState來修改url,該事件則不會觸發,因此,我們可以把它用作瀏覽器的前進后退事件。該事件有一個參數,就是上文pushState方法的第一個參數state。
一個實例:
這里我們以daipig為例,打開daipig,地址欄是http://www.daipig.com 。接下來打開F12 Console,輸入:
history.pushState({ a: 1, b: 2 }, null, "http://www.daipig.com/abcdefg");
可以發現,url已經變成我們輸入的url了,但頁面並沒有刷新,也沒有發出任何請求。現在再輸入history.state,就可以看到我們剛剛傳過來的第一個參數state了。
這時點擊后退,url會回到www.daipig.com,同樣是沒有刷新。只不過后退的時候其實是觸發了window.onpopstate事件的。
詳細文檔可以查閱MDN: https://developer.mozilla.org/zh-CN/docs/DOM/Manipulating_the_browser_history
怎么完整的實現Pjax?
Pjax的原理上文已經講了,並不復雜。我實現了一個比較粗糙的Pjax庫,已經能滿足不少需求,地址在文末。
完整的代碼見Github,這里我們只談需要注意的一些地方。
匹配選擇器
要實現Pjax,難免就會有匹配選擇器的需求。你需要判斷當前點擊的元素,是否匹配指定選擇器。這里我給出一個兼容至IE8的解決方法:
// 判斷element是否匹配選擇器selector function matchSelector(element, selector) { var match = document.documentElement.webkitMatchesSelector || document.documentElement.mozMatchesSelector || document.documentElement.msMatchesSelector || // 兼容IE8及以下瀏覽器 function(selector, element) { // 這是一個好方法,可惜IE8連indexOf都不支持 // return Array.prototype.indexOf.call(document.querySelectorAll(selector), this) !== -1; if (element.tagName === selector.toUpperCase()) return true; var elements = document.querySelectorAll(selector), length = elements.length; while (length--) { if (elements[length] === this) return true; } return false; }; // 重寫函數自身,使用閉包keep住match函數,不用每次都判斷兼容 matchSelector = function(element, selector) { return match.call(element, selector); }; return matchSelector(element, selector); } // 驗證一下 matchSelector(document.getElementById("abc"), "#abc"); // true matchSelector(document.querySelector("a"), "p"); // false
在現代瀏覽器上,優先使用原生的matchesSelector方法來判斷,在IE8及以下的瀏覽器里,循環document.querySelector的結果集,依次對比。
這個方法利用了閉包,然后重寫自身,只有在第一次調用時需要判斷加哪個前綴執行哪個方法,其后都是調用了閉包的match函數。
不支持HTML5 PushState的瀏覽器怎么辦?
IE6到IE9是不支持pushState的,要修改Url,只能利用Url的Hash,也即是#號。
你可以隨意找個網站試一下,在url后面加上#號和任意內容,頁面並不會刷新。此時點擊后退也只會回到上一條#號,同樣不會刷新。
那么我們只需把pushState(新url)換成localtion.hash = 新url,把onpopstate事件換成onhashchange事件就可以兼容IE了。 QQ音樂,網易雲音樂等就是使用這種方式。
二、只改變 URL Hash 的單頁面應用
仔細觀察可以發現,現在的 Twitter,Google,Facebook 的 URL 地址充斥着 #號或者 #! 號。例如一個新版 Twitter 地址是這樣的
https://twitter.com/#!/chloerei
這個 #! 號有什么意義呢?這個可以看看阮一峰整理的這篇《URL的井號》。這里假設你已經了解 # 號后面的改變不會導致頁面加載,怎么利用這個特性達到 1.3 提出的目標。
第一步,有關 Ajax 調用的鏈接全部用 #path 作為鏈接目標
例如,如果一個鏈接本來是
<a href="/topics/1">topic1</a>
就修改為
<a href="#/topics/1">topic1</a>
顯然,如果不做后續工作的話,這個鏈接點擊后頁面不會發生什么變化,用戶也不會被帶到新地址。唯一的改變是 URL 的 # 號部分變成了 #topics/1
設置 onhashchange 事件
在 javascript api 中,窗口 window 對象的 hash 值(# 號后面部分)發生變化時,會調用 onhashchange 事件。給 onhashchange 掛上一個 function,就可以在 hash 有改動的時候調用這個 function。
例如可以在控制台輸入這段 js 代碼測試
window.onhashchange = function(){alert(window.location.hash)}
實際中 function 里面就是放置真正用來刷新頁面的代碼了。比如在 jQuery 里用 $.get(location.path)。
效果
現在的 twitter,gooogle,facebook 都是使用這種方法刷新頁面,這對瀏覽器書簽、后退的支持也很好。
但是這有一些副作用。
一是因為頁面路徑被寫在了 Hash 里面,而瀏覽器是不向服務器發送 Hash 部分的。所以打開這樣的 URL 需要兩個來回:1、打開空白的首頁 2、根據 Hash 用 Ajax 載入實際內容
二是把路徑寫在了 Hash 里面破壞了 URL 原先的含義。從 URL 字面看
https://twitter.com/#!/chloerei
這個頁面表示的是 twitter.com 頁面上的 !/chloerie 錨點。但從實際內容上,這表示的是 chloerei 的個人頁面。所以有人稱這種站點為“單頁面應用”。總的來說,這個方案對主流瀏覽器的支持程度很高,是目前的主流方案。