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 参数
事件
1. 点击链接后触发的一系列事件, 除了 pjax:click 和 pjax:clicked 的事件源是点击的按钮,其他事件的事件源都是要替换内容的容器。可以在 pjax:start 事件触发时开始过度动画,在 pjax:end 事件触发时结束过度动画。
- 注意:
pjax:beforeReplace 事件前 pjax 会调用 extractContainer 函数处理页面内容,即以 script[src] 的形式引入的 js 脚本不会被重复加载,有必要可以改下源码。
2. 浏览器前进/后退导航时触发的事件(暂时没做过多研究)
服务端配置
我的项目是 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版本,有新模版发布时,通过配置文件修改版本来强制页面刷新 } } } }
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 标签,这样执行 pjax 请求时不仅地址栏 url 会变化,而且浏览器标签的标题内容也会变化。
针对没有服务端处理的方案如下:
// fragment一般同container一致 $(document).pjax('a[data-pjax]', '#main-content .wrapper', {fragment: '#main-content .wrapper'});
插件伴侣——NProgress
比较漂亮的一款进度条插件,用法十分简单,很适合做pjax的过度动画,详细用法在该项目 github 上有介绍
NProgress
- 示例:
$(document).on('pjax:start', NProgress.start).on('pjax:end', NProgress.done);
结语
虽然个人还是比较喜欢造轮子(有成就感),不怎么喜欢用插件(一般插件使用复杂,文档少学习成本大,还不如自己写),但看了 pjax 的源码后感觉真要自己也使用 pushState + ajax 的方式简单的实现它的功能,还是要踩不少坑的,所以为什么要放着这么个易用又精致的小轮子不用呢?我的项目是一个管理系统,统一的 左侧菜单 + 右侧table 的布局,每个页面都需要一个独立访问的 url,非常适合使用 pjax。由于使用的 velocity 模版技术,集成 pjax 就是分分钟的事,不仅对原先的代码完全没影响,还提升了加载速度,页面过度效果更好,再用上了 NProgress,感觉逼格又上升不少,哈哈。