pjax 即 pushState + ajax,它被封裝成了一個(gè) jQuery 擴(kuò)展以方便使用。pjax 主要用來解決 HTML 頁面局部刷新 url 不更新和不支持后退和前進(jìn)的問題,提升用戶體驗(yàn)。
pjax原理
pjax 的實(shí)現(xiàn)是利用 HTML5 的 pushState() 和 replaceState() 新特性和 ajax 結(jié)合實(shí)現(xiàn)。pushState() 和 replaceState() 用來操作 State(狀態(tài))對象,即可添加和修改歷史記錄,進(jìn)而更新 url 和提供前進(jìn)、后退操作 ajax 實(shí)現(xiàn)數(shù)據(jù)的異步加載進(jìn)而局部刷新。
工作流程圖
源碼分析
- pjax支持判斷
(function($){ $.support.pjax = window.history && window.history.pushState && window.history.replaceState && // pushState isn't reliable on iOS until 5. !navigator.userAgent.match(/((iPod|iPhone|iPad).+bOSs+[1-4]D|WebApps/.+CFNetwork)/) if ($.support.pjax){ enable() //啟用 } else { disable() //禁用 } })(jQuery)
- enable()
function enable() { $.fn.pjax = fnPjax //注冊jQuery的pjax方法 $.pjax = pjax //注冊pjax對象 $.pjax.enable = $.noop $.pjax.disable = disable $.pjax.click = handleClick //注冊click回調(diào) $.pjax.submit = handleSubmit //注冊submit回調(diào) $.pjax.reload = pjaxReload //注冊reload回調(diào) $.pjax.defaults = {} //設(shè)置默認(rèn)值 $(window).on('popstate.pjax', onPjaxPopstate) //綁定popstate事件回調(diào) }
$.noop
是一個(gè)空方法,不做任何事,即function(){}
。popstate.pjax
是 JS 事件的命名空間寫法,popstate
是事件類型,每當(dāng)激活的歷史記錄發(fā)生變化時(shí)(瀏覽器操作前進(jìn)、后退按鈕、調(diào)用 back() 或者 go() 方法),都會(huì)觸發(fā) popstate 事件,但調(diào)用 pushState()、replaceState() 不會(huì)觸發(fā) popstate 事件。.pjax
是該事件的命名空間,這樣方便解綁指定命名空間的事件響應(yīng),在綁定匿名函數(shù)時(shí)常使用,例如:this.on('click.pjax', selector, function(event){})
。
- fnPjax()
該方法返回一個(gè) jQuery 對象,等同于 $.fn.pjax。
return this.on('click.pjax', selector, function(event) { //獲取pjax配置信息 options = optionsFor(container, options) //自動(dòng)綁定click事件響應(yīng) return this.on('click.pjax', selector, function(event) { var opts = options if (!opts.container) { opts = $.extend({}, options) //如果不配置container,則默認(rèn)獲取data-pjax屬性值對應(yīng)的 opts.container = $(this).attr('data-pjax') } handleClick(event, opts) //調(diào)用click回調(diào) }) }
- pjax()
// Use it just like $.ajax: // // var xhr = $.pjax({ url: this.href, container: '#main' }) // console.log( xhr.readyState ) // // Returns whatever $.ajax returns. function pjax(options) { //獲取設(shè)置 options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) //判斷檢測 if (containerType !== 'string') /** * ajax響應(yīng)回調(diào)注冊 */ //beforeSend options.beforeSend = function(xhr, settings) { //設(shè)置pjax頭信息,供后端做兼容處理 xhr.setRequestHeader('X-PJAX', 'true') xhr.setRequestHeader('X-PJAX-Container', options.container) //設(shè)置超時(shí) } //complete options.complete = function(xhr, textStatus) { //綁定pjax:complete事件 fire('pjax:complete', [xhr, textStatus, options]) //綁定pjax:end事件 fire('pjax:end', [xhr, options]) } //error options.error = function(xhr, textStatus, errorThrown) { //綁定pjax:error事件 fire('pjax:error', [xhr, textStatus, errorThrown, options]) } //success,重點(diǎn) options.success = function(data, status, xhr) { //判斷檢測 if (currentVersion && latestVersion && currentVersion !== latestVersion) ... ... window.history.replaceState(pjax.state, container.title, container.url) //綁定pjax:beforeReplace事件 fire('pjax:beforeReplace', [container.contents, options], { state: pjax.state, previousState: previousState }) //渲染頁面 context.html(container.contents) //綁定pjax:success事件 fire('pjax:success', [data, status, xhr, options]) } //初始化ajax var xhr = pjax.xhr = $.ajax(options) if (xhr.readyState > 0) { //緩存頁面cache cachePush(pjax.state.id, [options.container, cloneContents(context)]) //pushState window.history.pushState(null, "", options.requestUrl) //綁定pjax:start事件 fire('pjax:start', [xhr, options]) //綁定pjax:send事件 fire('pjax:send', [xhr, options]) } //返回jQuery對象 return pjax.xhr }
- 回調(diào)函數(shù)
1) handleClick()
// Examples // // $(document).on('click', 'a', $.pjax.click) // // is the same as // $(document).pjax('a') // // Returns nothing. function handleClick(event, container, options) { options = optionsFor(container, options) //環(huán)境檢測 if (link.tagName.toUpperCase() !== 'A') ... ... //綁定pjax:click事件 var clickEvent = $.Event('pjax:click') $link.trigger(clickEvent, [opts]) //執(zhí)行pjax pjax(opts) //成功則阻止默認(rèn)行為 event.preventDefault() //綁定pjax:clicked事件 $link.trigger('pjax:clicked', [opts]) }
2)handleSubmit()
// Examples // // $(document).on('submit', 'form', function(event) { // $.pjax.submit(event, '[data-pjax-container]') // }) // // Returns nothing. function handleSubmit(event, container, options) { options = optionsFor(container, options) //環(huán)境檢測 if (form.tagName.toUpperCase() !== 'FORM') ... ... //默認(rèn)配置 var defaults = { type: ($form.attr('method') || 'GET').toUpperCase(), url: $form.attr('action'), container: $form.attr('data-pjax'), target: form } if (defaults.type !== 'GET' && window.FormData !== undefined) { //POST時(shí)data域 defaults.data = new FormData(form) } //執(zhí)行pjax pjax($.extend({}, defaults, options)) //成功則阻止默認(rèn)行為 event.preventDefault() }
3)pjaxReload()
// Reload current page with pjax. function pjaxReload(container, options) { var defaults = { //當(dāng)前url url: window.location.href, push: false, replace: true, scrollTo: false } //執(zhí)行pjax return pjax($.extend(defaults, optionsFor(container, options))) }
4)onPjaxPopstate()
// popstate handler takes care of the back and forward buttons function onPjaxPopstate(event) { //環(huán)境監(jiān)測 if (state && state.container) ... ... //獲取頁面cache var cache = cacheMapping[state.id] || [] //綁定pjax:popstate事件 var popstateEvent = $.Event('pjax:popstate', { state: state, direction: direction }) container.trigger(popstateEvent) if (contents) { //有頁面cache,直接渲染頁面 //綁定pjax:start事件 container.trigger('pjax:start', [null, options]) //綁定pjax:beforeReplace事件 var beforeReplaceEvent = $.Event('pjax:beforeReplace', { state: state, previousState: previousState }) container.trigger(beforeReplaceEvent, [contents, options]) //渲染頁面 container.html(contents) //綁定pjax:end事件 container.trigger('pjax:end', [null, options]) } else { //無頁面cache,執(zhí)行pjax pjax(options) } }
pjax使用
經(jīng)過上述分析,就可以很容易使用 pjax 了。
客戶端
pjax 支持 options 配置和事件機(jī)制。
- options配置
參數(shù)名 | 默認(rèn)值 | 說明 |
---|---|---|
timeout | 650 | ajax 超時(shí)時(shí)間(單位 ms),超時(shí)后會(huì)執(zhí)行默認(rèn)的頁面跳轉(zhuǎn),所以超時(shí)時(shí)間不應(yīng)過短,不過一般不需要設(shè)置 |
push | true | 使用 window.history.pushState 改變地址欄 url(會(huì)添加新的歷史記錄) |
replace | false | 使用 window.history.replaceState 改變地址欄 url(不會(huì)添加歷史記錄) |
maxCacheLength | 20 | 緩存的歷史頁面?zhèn)€數(shù)(pjax 加載新頁面前會(huì)把原頁面的內(nèi)容緩存起來,緩存加載后其中的腳本會(huì)再次執(zhí)行) |
version | 是一個(gè)函數(shù),返回當(dāng)前頁面的 pjax-version,即頁面中 標(biāo)簽內(nèi)容。使用 response.setHeader(“X-PJAX-Version”, “”) 設(shè)置與當(dāng)前頁面不同的版本號,可強(qiáng)制頁面跳轉(zhuǎn)而不是局部刷新 | |
scrollTo | 0 | 頁面加載后垂直滾動(dòng)距離(與原頁面保持一致可使過度效果更平滑) |
type | “GET” | ajax 的參數(shù),http 請求方式 |
dataType | “html” | ajax 的參數(shù),響應(yīng)內(nèi)容的 Content-Type |
container | 用于查找容器的 CSS 選擇器,[container] 參數(shù)沒有指定時(shí)使用 | |
url | link.href | 要跳轉(zhuǎn)的連接,默認(rèn) a 標(biāo)簽的 href 屬性 |
fragment | 使用響應(yīng)內(nèi)容的指定部分(css 選擇器)填充頁面,服務(wù)端不進(jìn)行處理導(dǎo)致全頁面請求的時(shí)候需要使用該參數(shù),簡單的說就是對請求到的頁面做截取 |
- pjax事件
為了方便擴(kuò)展,pjax 支持一些預(yù)定義的事件。
事件名 | 支持取消 | 參數(shù) | 說明 |
---|---|---|---|
pjax:click | ✔ | options | 點(diǎn)擊按鈕時(shí)觸發(fā)??烧{(diào)用 e.preventDefault() 取消 pjaxa |
pjax:beforeSend | ✔ | xhr, options | ajax 執(zhí)行 beforeSend 函數(shù)時(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 請求超時(shí)后觸發(fā)。可調(diào)用 e.preventDefault() 繼續(xù)等待 ajax 請求結(jié)束 |
pjax:error | ✔ | xhr, textStatus, error, options | ajax 請求失敗后觸發(fā)。默認(rèn)失敗后會(huì)跳轉(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:popstate | forward / back(前進(jìn)/后退) | ||
pjax:start | null, options | pjax開始 | |
pjax:beforeReplace | contents, options | 內(nèi)容替換渲染前觸發(fā),如果緩存了要導(dǎo)航頁面的內(nèi)容則使用緩存,否則使用pjax加載 | |
pjax:end | null, options | pjax結(jié)束 |
客戶端通過以下 2 個(gè)步驟就可以使用 pjax :
- 引入jquery 和 jquery.pjax.js
- 注冊事件
JS
<script src="jquery.pjax.js"></script> /** * 方式1 監(jiān)聽按鈕父節(jié)點(diǎn)事件 */ $(document).pjax(selector, [container], options); /** * 方式2 直接監(jiān)聽按鈕,可以不用指定容器,默認(rèn)使用按鈕的data-pjax屬性值查找容器 */ $("a[data-pjax]").pjax(); /** * 方式3 主動(dòng)綁定點(diǎn)擊事件監(jiān)聽 */ $(document).on('click', 'a', $.pjax.click); $(document).on('click', 'a', function(event) { //獲取container var container = $(this).closest('[data-pjax-container]'); //click回調(diào) $.pjax.click(event, container); }); /** * 方式4 主動(dòng)綁定表單提交事件監(jiān)聽 */ $(document).on('submit', 'form', function(event) { //獲取container var container = $(this).closest('[data-pjax-container]'); //submit回調(diào) $.pjax.submit(event, container); }); /** * 方式5 加載內(nèi)容到指定容器 */ $.pjax({url: this.href, container: '#main'}); /** * 方式6 重新加載當(dāng)前頁面容器的內(nèi)容 */ $.pjax.reload('#container');
YII
在 Yii 中,已經(jīng)將 pjax 封裝成了 widgets,故在渲染時(shí)如下使用即可:
//view <?php Pjax::begin(); ?> ... ... <?php Pjax::end(); ?>
pjax 封裝成的 widgets 源碼文件widgets/Pjax.php
,事件注冊部分如下:
public function registerClientScript() { //a標(biāo)簽的click if ($this->linkSelector !== false) { $linkSelector = Json::htmlEncode($this->linkSelector !== null ? $this->linkSelector : '#' . $id . ' a'); $js .= "jQuery(document).pjax($linkSelector, "#$id", $options);"; } //form表單的submit if ($this->formSelector !== false) { $formSelector = Json::htmlEncode($this->formSelector !== null ? $this->formSelector : '#' . $id . ' form[data-pjax]'); $submitEvent = Json::htmlEncode($this->submitEvent); $js .= "njQuery(document).on($submitEvent, $formSelector, function (event) {jQuery.pjax.submit(event, '#$id', $options);});"; } $view->registerJs($js); }
服務(wù)端
由于只是 HTML5 支持 pjax,所以后端需要做兼容處理。通過 X-PJAX
頭信息可得知客戶端是否支持 pjax,如果支持,則只返回局部頁面,否則 a 鏈接默認(rèn)跳轉(zhuǎn),返回整個(gè)頁面。
/** * IndexController示例 */ public function actionIndex() { $dataProvider = new CActiveDataProvider('Article', array( 'criteria' => array('order' => 'create_time DESC') )); //存在X-Pjax頭,支持pjax if (Yii::$app->getRequest()->getHeaders()->get('X-Pjax')) { //返回局部頁面 $this->renderPartial('index', array( 'dataProvider' => $dataProvider, )); } else { //返回整個(gè)頁面 $this->render('index', array( 'dataProvider' => $dataProvider, )); } }
pjax失效情況
在以下 9 種情況時(shí)候 pjax 會(huì)失效,源碼部分如下:
//click回調(diào) function handleClick(event, container, options) { ... // 1. 點(diǎn)擊的事件源不是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)滾輪點(diǎn)擊、點(diǎn)擊超鏈接的同時(shí)按下Shift、Ctrl、Alt和Meta if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return // 3. 跨域 if (location.protocol !== link.protocol || location.hostname !== link.hostname) return // 4. 當(dāng)前頁面的錨點(diǎn)定位 if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location)) return // 5. 已經(jīng)阻止元素發(fā)生默認(rèn)的行為 if (event.isDefaultPrevented()) return ... var clickEvent = $.Event('pjax:click') $(link).trigger(clickEvent, [opts]) // 6. pjax:click事件回調(diào)中已經(jīng)阻止元素發(fā)生默認(rèn)的行為 if (!clickEvent.isDefaultPrevented()) { pjax(opts) } } //pjax function pjax(options) { options.beforeSend = function(xhr, settings) { //7. ajx超時(shí) timeoutTimer = setTimeout(function() { if (fire('pjax:timeout', [xhr, options])) xhr.abort('timeout') }, settings.timeout) } options.success = function(data, status, xhr) { //8. 當(dāng)前頁面和請求的新頁面版本不一致 if (currentVersion && latestVersion && currentVersion !== latestVersion) { return } //9. ajax失敗 context.html(container.contents) }
其他方案
除了使用 pjax 解決局部刷新并支持前進(jìn)和后退問題外,也可以使用 browserstate/history.js + ajax 方案來實(shí)現(xiàn)
推薦教程:《PHP》《JS教程》