上周看到一篇文章在分析简书个人主页
页面3个tab页切换的bug,起先觉得是寻常的样式bug而已没怎么在乎,后来在文章中看到pjax
这个术语,长得和ajax
有点像,遂去了解了下。javascript
虽然传统的ajax方式能够异步无刷新改变页面内容,但没法改变页面URL,所以有种方案是在内容发生改变后经过改变URL的hash的方式得到更好的可访问性(如https://liyu365.github.io/BG-UI/tpl/#page/desktop.html),可是hash的方式有时候不能很好的处理浏览器的前进、后退,并且常规代码要切换到这种方式还要作很多额外的处理。而pjax的出现就是为了解决这些问题,简单的说就是对ajax的增强。css
pjax结合pushState和ajax技术, 不须要从新加载整个页面就能从服务器加载Html到你当前页面,这个ajax请求会有永久连接、title并支持浏览器的回退/前进按钮。html
pjax项目地址在 https://github.com/defunkt/jquery-pjax 。 实际的效果见:https://pjax.herokuapp.com/ 没有勾选pjax的时候点击连接是跳转的, 勾选了以后连接都是变成了ajax刷新(实际效果以下图的请求内容对比)。前端
按需请求,每次只需加载页面的部份内容,而不用重复加载一些公共的资源文件和不变的页面结构,大大减少了数据请求量,以减轻对服务器的带宽和性能压力,还大大提高了页面的加载速度。html5
常规页面跳转须要从新加载画面上的内容,会有明显的闪烁,并且每每和跳转前的页面没有连贯性,用户体验不是很好。若是再赶上页面比较庞大、网速又不是很好的状况,用户体验就更加雪上加霜了。使用pjax后,因为只刷新部分页面,切换效果更加流畅,并且能够定制过分动画,在等待页面加载的时候体验就比较舒服了。java
pjax使用了pushState来改变地址栏的url,这是html5中history的新特性,在某些旧版浏览器中可能不支持。不过pjax会进行判断,功能不适用的时候会执行默认的页面跳转操做。jquery
要作到普通请求返回完整页面,而pjax请求只返回部分页面,服务端就须要作一些特殊处理,固然这对于设计良好的后端框架来讲,添加一些统一处理仍是比较容易的,天然也没太大问题。另外,即便后台不作处理,设置pjax的fragment参数来达到一样的效果。git
综合来看,pajx的优势很强势,缺点也几乎能够忽略,仍是很是值得推荐的,尤为是相似博客这种大部分状况下只有主体内容变化的网站。关键它使用简单、学习成本小,即时全站只有极个别页面能用获得,尝试下没什么损失。pjax的github主页介绍的已经很详细了,想了解更多能够看下源码。github
/** * 方式一 按钮父节点监听事件 * * @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');
参数名 | 默认值 | 说明 |
---|---|---|
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选择器)填充页面,服务端不进行处理致使全页面请求的时候须要使用该参数,简单的说就是对请求到的页面作截取 |
除了上述参数外,ajax的一些参数也是能够设置在这里的,不过通常没什么必要。// ajax 最终参数: options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options);
web
会有一些状况致使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]) } }
除了上述状况以外,还有下列几种状况:
事件名 | 支持取消 | 参数 | 说明 |
---|---|---|---|
pjax:click | ✔ |
options | 点击按钮时触发。可调用e.preventDefault(); 取消pjax |
pjax:beforeSend | ✔ |
xhr, options | ajax执行beforeSend 函数时触发,可在回调函数中设置额外的请求头参数。可调用e.preventDefault(); 取消pjax |
pjax: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:beforeReplace事件前pjax会调用extractContainer函数处理页面内容,即script[src]形式引入的js脚本不会被重复加载,有必要能够改下源码。
事件名 | 参数 | 说明 |
---|---|---|
pjax:popstate | 页面导航方向: 'forward'/'back'(前进/后退) | |
pjax:start | null, options | pjax开始 |
pjax:beforeReplace | contents, options | 内容替换渲染前触发,若是缓存了要导航页面的内容则使用缓存,不然使用pjax加载 |
pjax:end | null, options | pjax结束 |
个人项目是spring MVC + velocity 的组合,这里就以此为例子,其余语言和框架的服务端能够参考下这里的思路。
项目中使用的识图解析器是org.springframework.web.servlet.view.velocity.VelocityLayoutViewResolver
这个类,好处是可使用模版技术,每一个页面能够只写主体内容,公共部分统一写在模版里面,是否是和pjax绝配哈!pjax.js默认会在请求头加入X_PJAX
字段,并置为true,因此以此来判断是否pjax请求。对于普通的请求使用常规的模版,pjax请求则使用空模版或者特定的模版。
常规模版内容:
<!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 ##页面内容 </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 方法调用以后,页面渲染前执行 * * @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);// 设置当前页面的pjax版本 if (isPajx) { model.addAttribute("layout", "layout_pjax.vm");// 指定pjax请求时使用的模版 // 在vm页面中经过 #set($layout = 'xxx.vm') 的方式指定模版 response.setHeader("X-PJAX-Version", X_PJAX_VERSION);// 响应内容的pjax版本,有新模版发布时,经过配置文件修改版原本强制页面刷新 } } } }
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <bean id="pjaxInterceptor" class="xxx.PjaxInterceptor"/> </mvc:interceptor> </mvc:interceptors>
<title>$!{title}</title> $screen_content
模版中使用title标签,这样pjax请求时不只地址栏url会变化,并且浏览器标签的标题内容也会变化。$(document).pjax('a[data-pjax]', '#main-content .wrapper', {fragment: '#main-content .wrapper'});// fragment通常同container一致
比较漂亮的一款进度条插件,用法十分简单,很适合作pjax的过分动画,详细用法在该项目github上有介绍
$(document).on('pjax:start', NProgress.start).on('pjax:end', NProgress.done);
虽然我的仍是比较喜欢造轮子(有成就感),不怎么喜欢用插件(通常插件使用复杂,文档少学习成本大,还不如本身写),但看了pjax的源码后感受真要本身也使用pushState + ajax的方式简单的实现它的功能,仍是要踩很多坑的,因此为何要放着这么个易用又精致的小轮子不用呢?个人项目是一个管理系统,统一的左侧菜单+右侧table的布局,每一个页面都须要一个独立访问的url,很是适合使用pjax。因为使用的velocity模版技术,集成pjax就是分分钟的事,不只对原先的代码彻底没影响,还提高了加载速度和页面过分体验效果,再用上了NProgress,感受逼格又上升很多,哈哈。
前段时间工做比较忙很久没写文章了,这段时间有点闲下来就抽空学了些新东西记录下,对于此次的学习成果仍是比较满意的。( *^_^* )