前言
上周看到一篇文章在分析簡書 我的主頁 頁面 3 個 tab 頁切換的 bug,起先以為是尋常的樣式 bug 而已沒怎么在意,后來在文章中看到 pjax 這個術(shù)語,長得和 ajax 有點像,遂去了解了下。
簡介
雖然傳統(tǒng)的 ajax 方式可以異步無刷新改變頁面內(nèi)容,但無法改變頁面 URL,因此有種方案是在內(nèi)容發(fā)生改變后通過改變 URL 的 hash 的方式獲得更好的可訪問性(如 https://liyu365.github.io/BG-UI/tpl/#page/desktop.html),但是 hash 的方式有時候不能很好的處理瀏覽器的前進、后退,而且常規(guī)代碼要切換到這種方式還要做不少額外的處理。而 pjax 的出現(xiàn)就是為了解決這些問題,簡單的說就是對 ajax 的加強。
pjax 結(jié)合 pushState 和 ajax 技術(shù), 不需要重新加載整個頁面就能從服務(wù)器加載 Html 到你當(dāng)前頁面,這個 ajax 請求會有永久鏈接、title 并支持瀏覽器的回退/前進按鈕。
pjax 項目地址在 https://github.com/defunkt/jquery-pjax 。 實際的效果見: http://pjax.herokuapp.com 沒有勾選 pjax 的時候點擊鏈接是跳轉(zhuǎn)的, 勾選了之后鏈接都是變成了 ajax 刷新(實際效果如下圖的請求內(nèi)容對比)。
優(yōu)點:
- 減輕服務(wù)端壓力
按需請求,每次只需加載頁面的部分內(nèi)容,而不用重復(fù)加載一些公共的資源文件和不變的頁面結(jié)構(gòu),大大減小了數(shù)據(jù)請求量,以減輕對服務(wù)器的帶寬和性能壓力,還大大提升了頁面的加載速度。
- 優(yōu)化頁面跳轉(zhuǎn)體驗
常規(guī)頁面跳轉(zhuǎn)需要重新加載畫面上的內(nèi)容,會有明顯的閃爍,而且往往和跳轉(zhuǎn)前的頁面沒有連貫性,用戶體驗不是很好。如果再遇上頁面比較龐大、網(wǎng)速又不是很好的情況,用戶體驗就更加雪上加霜了。使用pjax后,由于只刷新部分頁面,切換效果更加流暢,而且可以定制過度動畫,在等待頁面加載的時候體驗就比較舒服了。
缺點:
- 不支持一些低版本的瀏覽器(如IE系列)
pjax使用了pushState來改變地址欄的url,這是html5中history的新特性,在某些舊版瀏覽器中可能不支持。不過pjax會進行判斷,功能不適用的時候會執(zhí)行默認的頁面跳轉(zhuǎn)操作。
- 使服務(wù)端處理變得復(fù)雜
要做到普通請求返回完整頁面,而pjax請求只返回部分頁面,服務(wù)端就需要做一些特殊處理,當(dāng)然這對于設(shè)計良好的后端框架來說,添加一些統(tǒng)一處理還是比較容易的,自然也沒太大問題。另外,即使后臺不做處理,設(shè)置pjax的fragment參數(shù)來達到同樣的效果。
綜合來看,pajx 的優(yōu)點很強勢,缺點也幾乎可以忽略,還是非常值得推薦的,尤其是類似博客這種大部分情況下只有主體內(nèi)容變化的網(wǎng)站。關(guān)鍵它使用簡單、學(xué)習(xí)成本小,即時全站只有極個別頁面能用得到,嘗試下沒什么損失。pjax 的 github 主頁介紹的已經(jīng)很詳細了,想了解更多可以看下源碼。
用法
- 引入 jquery 和 jquery.pjax.js
- 注冊事件
/**
* 方式一 按鈕父節(jié)點監(jiān)聽事件
*
* @param selector 觸發(fā)點擊事件的按鈕
* @param container 展示刷新內(nèi)容的容器,也就是會被替換的部分
* @param options 參數(shù)
*/
$(document).pjax(selector, [container], options);
// 方式二 直接對按鈕監(jiān)聽,可以不用指定容器,使用按鈕的data-pjax屬性值查找容器
$("a[data-pjax]").pjax();
// 方式三 常規(guī)的點擊事件監(jiān)聽方式
$(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);
});
// 加載內(nèi)容到指定容器
$.pjax({ url: this.href, container: '#main' });
// 重新當(dāng)前頁面容器的內(nèi)容
$.pjax.reload('#container');
options默認參數(shù)說明
參數(shù)名 | 默認值 | 說明 |
---|---|---|
timeout | 650 |
ajax 超時時間(單位 ms ),超時后會執(zhí)行默認的頁面跳轉(zhuǎn),所以超時時間不應(yīng)過短,不過一般不需要設(shè)置 |
push | true | 使用 window.history.pushState 改變地址欄 url ( 會添加新的歷史記錄 ) |
replace | false | 使用 window.history.replaceState 改變地址欄 url ( 不會添加歷史記錄 ) |
maxCacheLength | 20 | 緩存的歷史頁面?zhèn)€數(shù)( pjax 加載新頁面前會把原頁面的內(nèi)容緩存起來,緩存加載后其中的腳本會再次執(zhí)行 ) |
version | 是一個函數(shù),返回當(dāng)前頁面的pjax-version,即頁面中 <meta http-equiv="x-pjax-version"> 標(biāo)簽內(nèi)容。使用 response.setHeader("X-PJAX-Version", "") 設(shè)置與當(dāng)前頁面不同的版本號,可強制頁面跳轉(zhuǎn)而不是局部刷新。 |
|
scrollTo | 0 | 頁面加載后垂直滾動距離( 與原頁面保持一致可使過度效果更平滑 ) |
type | "GET" |
ajax 的參數(shù),http 請求方式 |
dataType | "html" |
ajax 的參數(shù),響應(yīng)內(nèi)容的 Content-Type
|
container | 用于查找容器的 CSS 選擇器 ,[container] 參數(shù)沒有指定時使用 |
|
url | link.href | 要跳轉(zhuǎn)的連接,默認 a 標(biāo)簽 的 href 屬性 |
target | link |
pjax 事件參數(shù) e 的 relatedTarget 屬性,默認為點擊的 a 標(biāo)簽
|
fragment | 使用響應(yīng)內(nèi)容的指定部分( CSS 選擇器 )填充頁面,服務(wù)端不進行處理導(dǎo)致全頁面請求的時候需要使用該參數(shù),簡單的說就是對請求到的頁面做截取 |
除了上述參數(shù)外,ajax 的一些參數(shù)也是可以設(shè)置在這里的,不過一般沒什么必要。
// ajax 最終參數(shù):
options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options);
pjax失效情況
會有一些情況導(dǎo)致 pjax 失效,下面結(jié)合源碼分析下(省略部分無關(guān)代碼)
function handleClick(event, container, options) {
...
// 1. 點擊事件的事件源不是a標(biāo)簽。使用a標(biāo)簽可以做到對舊版本瀏覽器的兼容,所以不建議使用其他標(biāo)簽注冊事件
if (link.tagName.toUpperCase() !== 'A')
throw "$.fn.pjax or $.pjax.click requires an anchor element"
// 2. 使用鼠標(biāo)滾輪點擊(新標(biāo)簽頁打開)
// 點擊超鏈接的同時按下Shift、Ctrl、Alt和Meta(在Windows鍵盤中是Windows鍵,在蘋果機中是Cmd鍵)
// 作用分別代表新窗口打開、新標(biāo)簽打開(不切換標(biāo)簽)、下載、新標(biāo)簽打開(切換標(biāo)簽)
if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
return
// 3. 跨域(網(wǎng)絡(luò)通訊協(xié)議,域名不一致)
if (location.protocol !== link.protocol || location.hostname !== link.hostname)
return
// 4. 當(dāng)前頁面的錨點定位
if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location))
return
// 5. 已經(jīng)阻止元素發(fā)生默認的行為(url跳轉(zhuǎn))
if (event.isDefaultPrevented())
return
...
var clickEvent = $.Event('pjax:click')
$(link).trigger(clickEvent, [opts])
// 6. pjax:click事件回調(diào)中已經(jīng)阻止元素發(fā)生默認的行為(url跳轉(zhuǎn))
if (!clickEvent.isDefaultPrevented()) {
pjax(opts)
event.preventDefault()// 阻止url跳轉(zhuǎn)
$(link).trigger('pjax:clicked', [opts])
}
}
除了上述情況之外,還有下列幾種情況:
- ajax 請求失敗,或者 timeout 后請求被中止
- 當(dāng)前頁面的
X-PJAX-Version
和請求的新頁面版本不一致 - 請求得到完整的頁面(包含 html 標(biāo)簽)卻沒設(shè)置
fragment
參數(shù)
事件
1. 點擊鏈接后觸發(fā)的一系列事件, 除了 pjax:click
和 pjax:clicked
的事件源是點擊的按鈕,其他事件的事件源都是要替換內(nèi)容的容器??梢栽?pjax:start
事件觸發(fā)時開始過度動畫,在 pjax:end
事件觸發(fā)時結(jié)束過度動畫。
事件名 | 支持取消 | 參數(shù) | 說明 |
---|---|---|---|
pjax:click | ? |
options | 點擊按鈕時觸發(fā)。可調(diào)用 e.preventDefault(); 取消pjax
|
pjax:beforeSend | ? |
xhr, options |
ajax 執(zhí)行 beforeSend 函數(shù)時觸發(fā),可在回調(diào)函數(shù)中設(shè)置額外的請求頭參數(shù)。可調(diào)用 e.preventDefault(); 取消 pjax
|
pjax:start | xhr, options |
pjax 開始(與服務(wù)器連接建立后觸發(fā)) |
|
pjax:send | xhr, options |
pjax:start 之后觸發(fā) |
|
pjax:clicked | options |
ajax 請求開始后觸發(fā) |
|
pjax:beforeReplace | contents, options |
ajax 請求成功,內(nèi)容替換渲染前觸發(fā) |
|
pjax:success | data, status, xhr, options | 內(nèi)容替換成功后觸發(fā) | |
pjax:timeout | ? |
xhr, options |
ajax 請求超時后觸發(fā)??烧{(diào)用 e.preventDefault(); 繼續(xù)等待 ajax 請求結(jié)束 |
pjax:error | ? |
xhr, textStatus, error, options |
ajax 請求失敗后觸發(fā)。默認失敗后會跳轉(zhuǎn) url,如要阻止跳轉(zhuǎn)可調(diào)用 e.preventDefault();
|
pjax:complete | xhr, textStatus, options |
ajax 請求結(jié)束后觸發(fā),不管成功還是失敗 |
|
pjax:end | xhr, options |
pjax 所有事件結(jié)束后觸發(fā) |
- 注意:
pjax:beforeReplace
事件前 pjax 會調(diào)用extractContainer
函數(shù)處理頁面內(nèi)容,即以script[src]
的形式引入的js
腳本不會被重復(fù)加載,有必要可以改下源碼。
2. 瀏覽器前進/后退導(dǎo)航時觸發(fā)的事件(暫時沒做過多研究)
事件名 | 參數(shù) | 說明 |
---|---|---|
pjax:popstate | 頁面導(dǎo)航方向: 'forward'/'back'(前進/后退) | |
pjax:start | null, options |
pjax 開始 |
pjax:beforeReplace | contents, options | 內(nèi)容替換渲染前觸發(fā),如果緩存了要導(dǎo)航頁面的內(nèi)容則使用緩存,否則使用 pjax 加載 |
pjax:end | null, options |
pjax 結(jié)束 |
服務(wù)端配置
我的項目是 Spring MVC + velocity 的組合,這里就以此為例子,其他語言和框架的服務(wù)端可以參考下這里的思路。
項目中使用的視圖解析器是 org.springframework.web.servlet.view.velocity.VelocityLayoutViewResolver
這個類,好處是可以使用模版技術(shù),每個頁面可以只寫主體內(nèi)容,公共部分統(tǒng)一寫在模版里面,是不是和 pjax 絕配哈!pjax.js 默認會在請求頭加入 X_PJAX
字段,并置為 true
,所以以此來判斷是否 pjax 請求。對于普通的請求使用常規(guī)的模版,pjax 請求則使用空模版或者特定的模版。
- 常規(guī)模版內(nèi)容:
<!doctype html>
<html>
#set($basePath = "screen/contain")
<head>
<meta http-equiv="x-pjax-version" content="$!{X-PJAX-Version}"/>
#parse("$basePath/html-head.vm")
</head>
<body>
<section id="container">
#parse("$basePath/frame-head.vm")
#parse("$basePath/frame-left.vm")
<section id="main-content">
<section class="wrapper">
$screen_content ##頁面內(nèi)容
</section>
</section>
#parse("$basePath/frame-bottom.vm")
</section>
</body>
</html>
- 添加 SpringMVC 中的
Interceptor 攔截器
,用于后端渲染前插入 pjax 處理
public class PjaxInterceptor extends HandlerInterceptorAdapter {
@Value("${X-PJAX-Version}")
private String X_PJAX_VERSION;
/**
* Controller 方法調(diào)用之后,頁面渲染前執(zhí)行
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (modelAndView != null) {
boolean isPajx = Boolean.parseBoolean(request.getHeader("X-PJAX"));// 值為true表示pjax請求,這是重點
ModelMap model = modelAndView.getModelMap();
model.addAttribute("X-PJAX-Version", X_PJAX_VERSION);// 設(shè)置當(dāng)前頁面的pjax版本
if (isPajx) {
model.addAttribute("layout", "layout_pjax.vm");// 指定pjax請求時使用的模版
// 在vm頁面中通過 #set($layout = 'xxx.vm') 的方式指定模版
response.setHeader("X-PJAX-Version", X_PJAX_VERSION);// 響應(yīng)內(nèi)容的pjax版本,有新模版發(fā)布時,通過配置文件修改版本來強制頁面刷新
}
}
}
}
- xml 配置
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean id="pjaxInterceptor" class="xxx.PjaxInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
- pjax 請求模版頁面:layout_pjax.vm
<title>$!{title}</title>
$screen_content
模版中使用 title
標(biāo)簽,這樣執(zhí)行 pjax 請求時不僅地址欄 url 會變化,而且瀏覽器標(biāo)簽的標(biāo)題內(nèi)容也會變化。
針對沒有服務(wù)端處理的方案如下:
// fragment一般同container一致
$(document).pjax('a[data-pjax]', '#main-content .wrapper', {fragment: '#main-content .wrapper'});
插件伴侶——NProgress
比較漂亮的一款進度條插件,用法十分簡單,很適合做pjax的過度動畫,詳細用法在該項目 github 上有介紹
- 示例:
$(document).on('pjax:start', NProgress.start).on('pjax:end', NProgress.done);
結(jié)語
雖然個人還是比較喜歡造輪子(有成就感),不怎么喜歡用插件(一般插件使用復(fù)雜,文檔少學(xué)習(xí)成本大,還不如自己寫),但看了 pjax 的源碼后感覺真要自己也使用 pushState + ajax 的方式簡單的實現(xiàn)它的功能,還是要踩不少坑的,所以為什么要放著這么個易用又精致的小輪子不用呢?我的項目是一個管理系統(tǒng),統(tǒng)一的 左側(cè)菜單 + 右側(cè)table 的布局,每個頁面都需要一個獨立訪問的 url,非常適合使用 pjax。由于使用的 velocity 模版技術(shù),集成 pjax 就是分分鐘的事,不僅對原先的代碼完全沒影響,還提升了加載速度,頁面過度效果更好,再用上了 NProgress,感覺逼格又上升不少,哈哈。
前段時間工作比較忙好久沒寫文章了,這段時間有點閑下來就抽空學(xué)了些新東西記錄下,對于這次的學(xué)習(xí)成果還是比較滿意的。( *_* )