使用Pjax完成局部刷新且能浏览器回退
前言
现在很多的网站点击某个链接(比如导航)后原本应该是刷新页面的某个部分,但是却整个页面都刷新了。虽然使用Ajax
可以实现异步无刷新地改变页面内容,但是有两个问题:
- 无法改变浏览器地址栏上的页面URL
- 无法处理浏览器的前进后退
虽然有种解决方案是通过改变URL的hash方式,但是hash的URL方式会使得URL变得复杂,有时候也不能很好地处理浏览器前进,后退,而且常规代码要切换到这种方式还要做不少额外的处理。
对于这样的场景,Pjax
是最好的解决方案。
Pjax
简介
-
原理
基于Ajax
+history.pushState
的新技术,不需要重新加载整个页面就能从服务器加载Html
到你当前页面,这个Ajax
请求会有永久链接、title
并支持浏览器的回退/前进按钮。HTML5的新API扩展了window.history
,使历史记录点更加开放了。可以存储当前历史记录点、替换当前历史记录点、监听历史记录点。
-
优点
- 减轻服务端压力
按需请求,每次只需加载部分内容,而不用重复加载一些公共的资源文件和不变的页面结构,减小了数据请求量,减轻了对服务器的带宽和性能压力,大大提升了页面的加载速度。 - 优化页面跳转体验
常规页面跳转需要重新加载画面上的内容,会有明显的闪烁,而且往往和跳转前的页面没有连贯性,用户体验不是很好。如果再遇上页面比较庞大、网速又不是很好的情况,用户体验就更加雪上加霜了。使用pjax
后,由于只刷新部分页面,切换效果更加流畅,而且可以定制过度动画,在等待页面加载的时候体验就比较舒服了。
- 减轻服务端压力
-
缺点
- 不支持一些低版本的浏览器(如IE系列)
pjax
使用了pushState
来改变地址栏的url
,这是html5
中history
的新特性,在某些旧版浏览器中可能不支持。不过pjax
会进行判断,功能不适用的时候会执行默认的页面跳转操作。 - 使服务端处理变得复杂
要做到普通请求返回完整页面,而pjax
请求只返回部分页面,服务端就需要做一些特殊处理,当然这对于设计良好的后端框架来说,添加一些统一处理还是比较容易的,自然也没太大问题。另外,即使后台不做处理,设置pjax
的fragment
参数来达到同样的效果。
- 不支持一些低版本的浏览器(如IE系列)
综合来看,pajx
的优点很强势,缺点也几乎可以忽略,强烈推荐。
jquery.pjax
jquery.pjax
是一个jquery版的pjax
的实现方案,github地址是Jquery.pjax github官方仓库
- 使用方法
在页面上引入jquery
和jquery.pjax
:
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jquery.pjax/2.0.1/jquery.pjax.min.js"></script>
初始化$.fn.pjax
下面代码表示:当selector被点击时,执行ajax请求,并将返回的HTML字符串填充在container标记的位置。
//$(document).pjax(selector, [container], options)
$(document).pjax('a[data-pjax]', '#pjax-container', options)
参数说明
- selector:click事件的选择器
- container:pjax容器id
- options :配置参数
options
默认参数说明
参数名 | 默认值 | 说明 |
---|---|---|
timeout | 650 |
ajax 超时时间(单位 ms ),超时后会执行默认的页面跳转,所以超时时间不应过短,不过一般不需要设置 |
push | true | 使用 window.history.pushState 改变地址栏 url (会添加新的历史记录) |
replace | false | 使用 window.history.replaceState 改变地址栏 url (不会添加历史记录) |
maxCacheLength | 20 | 缓存的历史页面个数(pjax 加载新页面前会把原页面的内容缓存起来,缓存加载后其中的脚本会再次执行) |
version | 是一个函数,返回当前页面的pjax-version,即页面中 <meta http-equiv="x-pjax-version"> 标签内容。使用 response.setHeader("X-PJAX-Version", "") 设置与当前页面不同的版本号,可强制页面跳转而不是局部刷新。 |
|
scrollTo | 0 | 页面加载后垂直滚动距离(与原页面保持一致可使过度效果更平滑) |
type | "GET" |
ajax 的参数,http 请求方式 |
dataType | "html" |
ajax 的参数,响应内容的 Content-Type
|
container | 用于查找容器的CSS选择器,[container]参数没有指定时使用 | |
url | link.href | 要跳转的连接,默认a标签的href属性 |
target | link | pjax事件参数e的 relatedTarget 属性,默认为点击的 a标签
|
fragment | 使用响应内容的指定部分(css选择器)填充页面,服务端不进行处理导致全页面请求的时候需要使用该参数,简单的说就是对请求到的页面做截取 |
服务端设置
服务端只需要监听request
的headers
,查询是否包含x-pjax
字段,没有则渲染完整的html
页面,有的话则只渲染部分html
片段。这是在浏览器端查看到的request
字段:
Accept: text/html, */*; q=0.01
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: pjax.herokuapp.com
Pragma: no-cache
Referer: https://pjax.herokuapp.com/dinosaurs.html
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
X-PJAX: true
X-PJAX-Container: #main
X-Requested-With: XMLHttpRequest
pjax
失效情况
会有一些情况导致pjax
失效,结合源码分析下:
function handleClick(event, container, options) {
...
// 1. 点击事件的事件源不是a标签。使用a标签可以做到对旧版本浏览器的兼容,所以不建议使用其他标签注册事件
if (link.tagName.toUpperCase() !== 'A')
throw "$.fn.pjax or $.pjax.click requires an anchor element"
// 2. 使用鼠标滚轮点击(新标签页打开)
// 点击超链接的同时按下Shift、Ctrl、Alt和Meta(在Windows键盘中是Windows键,在苹果机中是Cmd键)
// 作用分别代表新窗口打开、新标签打开(不切换标签)、下载、新标签打开(切换标签)
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. 当前页面的锚点定位
if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location))
return
// 5. 已经阻止元素发生默认的行为(url跳转)
if (event.isDefaultPrevented())
return
...
var clickEvent = $.Event('pjax:click')
$(link).trigger(clickEvent, [opts])
// 6. pjax:click事件回调中已经阻止元素发生默认的行为(url跳转)
if (!clickEvent.isDefaultPrevented()) {
pjax(opts)
event.preventDefault()// 阻止url跳转
$(link).trigger('pjax:clicked', [opts])
}
}
除了上述情况之外,还有下列几种情况:
ajax
请求失败,或者timeout
后请求被中止- 当前页面的
X-PJAX-Version
和请求的新页面版本不一致 - 请求得到完整的页面(包含
html
标签)却没设置fragment
参数
Pjax
的生命周期
pjax:click
点击按钮时触发。可调用e.preventDefault()
取消pjaxpjax:beforeSend
xhr, options ajax 执行 beforeSend 函数时触发,可在回调函数中设置额外的请求头参数。可调用e.preventDefault();
取消 pjaxpjax:start
xhr, options pjax开始(与服务器连接建立后触发)pjax:send
xhr, options pjax:start 之后触发pjax:clicked
options ajax 请求开始后触发pjax:beforeReplace
contents, options ajax 请求成功,内容替换渲染前触发pjax:success
data, status, xhr, options 内容替换成功后触发pjax:timeout
xhr, options ajax请求超时后触发。可调用 e.preventDefault(); 继续等待 ajax 请求结束pjax:error
xhr, textStatus, error, options ajax 请求失败后触发。默认失败后会跳转url,如要阻止跳转可调用 e.preventDefault();pjax:complete
xhr, textStatus, options ajax 请求结束后触发,不管成功还是失败pjax:end
xhr, options pjax 所有事件结束后触发
然后我们在页面上就可以监听pjax
的生命周期,从而做出相应的动作:
$(document).on('pjax:send', function() {
$('#loading').show()
})
$(document).on('pjax:complete', function() {
$('#loading').hide()
})
API介绍
-
$.pjax.click
点击事件
if ($.support.pjax) {
$(document).on('click', 'a[data-pjax]', function(event) {
var container = $(this).closest('[data-pjax-container]')
var containerSelector = '#' + container.id
$.pjax.click(event, {container: containerSelector})
})
}
-
$.pjax.submit
提交表单
$(document).on('submit', 'form[data-pjax]', function(event) {
$.pjax.submit(event, '#pjax-container')
})
-
对当前URL使用pjax的方式重新获取HTML代码片段,并且在指定容器替换,这个过程不添加新的历史记录。(子片段重刷新)
$.pjax.reload('#pjax-container', options)
-
$.pjax
不是通过click触发pjax的时候使用。比如某些操作后自动触发pjax的过程。如果能获取到click的event事件时,建议使用$.pjax-click(event)替换。
强制reload
当使用pjax导致整个页面被强制刷时,可能的原因是:
- 当返回的HTML片段包含
<html>
标签且fragment
选择器没有指定时。如果指定了fragment
选择器,pjax将从HTML文档中提取需要局部刷新的子片段。 - 服务端返回的内容为空时。
- HTTP响应的code是 4xx 或者 5xx。
浏览器重定向
在响应头中设置X-PJAX-URL
,例如:
request.headers['X-PJAX-URL'] = "http://example.com/hello"
Layout重新加载
当客户端页面的pjax版本和服务器返回的pjax版本不一致时,页面会重新刷新。
客户端页面的pjax
版本:
<meta http-equiv="x-pjax-version" content="v123">
如果服务器修改了版本则重新刷新:
response.headers['X-PJAX-Version'] = "xxxx修改版本名称xxxx"
重新初始化
pjax
只是请求HTML片段之后插入指定位置,因此片段内的JS插件/组件初始化需要在pjax:end
事件后执行。
$(document).on('ready pjax:end', function(event) {
$(event.target).initializeMyPlugin()
})
这段代码会在document ready
或者container ready
后执行initializeMyPlugin
初始化方法
(包括前进后退)。