什麼是pjax?
現在很多網站( facebook, twitter)都支援這樣的一種浏覽方式, 當你點選一個站内的連結的時候, 不是做頁面跳轉, 而是隻是站内頁面重新整理。 這樣的使用者體驗, 比起整個頁面都閃一下來說, 好很多。 其中有一個很重要的組成部分, 這些網站的ajax重新整理是支援浏覽器曆史的, 重新整理頁面的同時, 浏覽器位址欄位上面的位址也是會更改, 用浏覽器的回退功能也能夠回退到上一個頁面。 那麼如果我們想要實作這樣的功能, 我們如何做呢? 我發現pjax提供了一個腳本支援這樣的功能。 pjax項目位址在 https://github.com/defunkt/jquery-pjax 。
為什麼要用pjax?
pjax有好幾個好處:
-
使用者體驗提升。
頁面跳轉的時候人眼需要對整個頁面作重新識别, 重新整理部分頁面的時候, 隻需要重新識别其中一塊區域。 同時, 由于重新整理部分頁面的時候提供了一個loading的提示, 以及在重新整理的時候舊頁面還是顯示在浏覽器中, 使用者能夠容忍更長的頁面加載時間。
-
極大地減少帶寬消耗和伺服器消耗。
由于隻是重新整理部分頁面, 大部分的請求(css/js)都不會重新擷取, 網站帶有使用者登入資訊的外框部分都不需要重新生成了。
如何使用pjax?
- 引入jquery和jquery.pjax.js
- 注冊事件
/**
* 方式一 按鈕父節點監聽事件
*
* @param selector 觸發點選事件的按鈕
* @param container 展示重新整理内容的容器,也就是會被替換的部分
* @param options 參數
*/
$(document).pjax(selector, [container], options);
// 方式二 直接對按鈕監聽,可以不用指定容器,使用按鈕的data-pjax屬性值查找容器
$("a[data-pjax]").pjax();
// 方式三 正常的點選事件監聽方式
$(document).on('click', 'a', $.pjax.click);
$(document).on('click', 'a', function(event) {
var container = $(this).closest('[data-pjax-container]');
$.pjax.click(event, container);
});
// 下列是源碼中介紹的其他用法,
// 表單送出
$(document).on('submit', 'form', function(event) {
var container = $(this).closest('[data-pjax-container]');
$.pjax.submit(event, container);
});
// 加載内容到指定容器
$.pjax({ url: this.href, container: '#main' });
// 重新目前頁面容器的内容
$.pjax.reload('#container');
具體文檔詳見:https://github.com/defunkt/jquery-pjax
pjax的原理
為了能夠處理問題, 我們需要能夠了解pjax的運作方式。 pjax的代碼隻有一個檔案: https://github.com/defunkt/jquery-pjax/blob/master/jquery.pjax.js 如果有能力, 可以自己去看一遍。 我這裡解釋一下原理。 首先, 我們在html裡面指定, 需要做pjax的連結内容是哪些, 以及點選之後需要更新的部分(放在data-pjax屬性裡面):
當加載了pjax腳本之後, 它會攔截這些連結的事件, 然後包裝成一個ajax請求, 發送給伺服器。
$.fn.pjax = function( container, options ) {
return this.live('click.pjax', function(event){
handleClick(event, container, options)
})
}
function handleClick(event, container, options) {
$.pjax($.extend({}, defaults, options))
...
event.preventDefault()
}
var pjax = $.pjax = function( options ) {
...
pjax.xhr = $.ajax(options)
}
這個請求帶有X-PJAX的HEADER辨別, 伺服器在收到這樣的請求的時候, 就知道隻需要渲染部分頁面傳回就可以了。
xhr.setRequestHeader('X-PJAX', 'true')
xhr.setRequestHeader('X-PJAX-Container', context.selector)
pjax接受到傳回的請求之後, 更新data-pjax指定的區域, 同時也會更新浏覽器的位址。
options.success = function(data, status, xhr) {
var container = extractContainer(data, xhr, options)
...
if (container.title) document.title = container.title
context.html(container.contents)
}
為了能夠支援浏覽器的後退, 利用到了history的api, 記錄下來對應的資訊,
pjax.state = {
id: options.id || uniqueId(),
url: container.url,
container: context.selector,
fragment: options.fragment,
timeout: options.timeout
}
if (options.push || options.replace) {
window.history.replaceState(pjax.state, container.title, container.url)
}
當浏覽器後退的時候, 攔截事件, 根據記錄的曆史資訊, 産生一個新的ajax請求。
$(window).bind('popstate', function(event){
var state = event.state
if (state && state.container) {
var container = $(state.container)
if (container.length) {
...
var options = {
id: state.id,
url: state.url,
container: container,
push: false,
fragment: state.fragment,
timeout: state.timeout,
scrollTo: false
}
if (contents) {
// pjax event is deprecated
$(document).trigger('pjax', [null, options])
container.trigger('pjax:start', [null, options])
// end.pjax event is deprecated
container.trigger('start.pjax', [null, options])
container.html(contents)
pjax.state = state
container.trigger('pjax:end', [null, options])
// end.pjax event is deprecated
container.trigger('end.pjax', [null, options])
} else {
$.pjax(options)
}
...
}
}
}
為了支援fallback, 一個是在加載的時候判斷浏覽器是否支援history push state API:
// Is pjax supported by this browser?
$.support.pjax =
window.history && window.history.pushState && window.history.replaceState
// pushState isn't reliable on iOS until 5.
&& !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[-]|WebApps\/.+CFNetwork)/)
另一個是當發現請求一段時間沒有回複的時候(可以設定參數timeout), 直接做頁面跳轉。
options.beforeSend = function(xhr, settings) {
if (settings.timeout > ) {
timeoutTimer = setTimeout(function() {
if (fire('pjax:timeout', [xhr, options]))
xhr.abort('timeout')
}, settings.timeout)
// Clear timeout setting so jquerys internal timeout isn't invoked
settings.timeout =
pjax失效情況
會有一些情況導緻pjax失效,下面結合源碼分析下(省略部分無關代碼)
function handleClick(event, container, options) {
...
// 點選事件的事件源不是a标簽。使用a标簽可以做到對舊版本浏覽器的相容,是以不建議使用其他标簽注冊事件
if (link.tagName.toUpperCase() !== 'A')
throw "$.fn.pjax or $.pjax.click requires an anchor element"
// 使用滑鼠滾輪點選(新标簽頁打開)
// 點選超連結的同時按下Shift、Ctrl、Alt和Meta(在Windows鍵盤中是Windows鍵,在蘋果機中是Cmd鍵)
// 作用分别代表新視窗打開、新标簽打開(不切換标簽)、下載下傳、新标簽打開(切換标簽)
if (event.which > || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
return
// 跨域(網絡通訊協定,域名不一緻)
if (location.protocol !== link.protocol || location.hostname !== link.hostname)
return
// 目前頁面的錨點定位
if (link.href.indexOf('#') > - && stripHash(link) == stripHash(location))
return
// 已經阻止元素發生預設的行為(url跳轉)
if (event.isDefaultPrevented())
return
...
var clickEvent = $.Event('pjax:click')
$(link).trigger(clickEvent, [opts])
// pjax:click事件回調中已經阻止元素發生預設的行為(url跳轉)
if (!clickEvent.isDefaultPrevented()) {
pjax(opts)
event.preventDefault()// 阻止url跳轉
$(link).trigger('pjax:clicked', [opts])
}
}
除了上述情況之外,還有下列幾種情況:
- ajax請求失敗,或者timeout後請求被中止
- 目前頁面X-PJAX-Version和請求的新頁面版本不一緻
- 請求得到完整的頁面(包含html标簽)卻沒設定fragment參數
Demo
index.php
<!DOCTYPE html>
<html>
<head>
<title>pjax</title>
<meta charset="utf-8">
</head>
<body>
<h1>My Site</h1>
<div>
Go to <a href="res1.php">第一頁</a>.<a href="res2.php">第二頁</a>
</div>
<div id="container"></div>
</body>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="jquery.pjax.js"></script>
<script type="text/javascript">
$(document).pjax('a', '#container')
</script>
</html>
res1.php
<?php
echo "<div style='background:red;'>第一頁</div>";
res2.php
<?php
echo "<div style='background:red;'>第二頁</div>";
效果:

Demo2
index.html
<!DOCTYPE html>
<html>
<head>
<title>pjax</title>
<meta charset="utf-8">
</head>
<body>
<h1>My Site</h1>
<div>
<input type="button" id="th" value="GO">
</div>
<div id="container"></div>
</body>
<script src="../jquery-2.1.4.min.js"></script>
<script src="../jquery.pjax.js"></script>
<script type="text/javascript">
$(function(){
$('#th').click(function(){
$.pjax({
url: './res3.php',
container: '#container'
});
});
});
</script>
</html>
re3.php
<?php
echo "<div style='background:red;'>第三頁</div>";