pjax 是一款可爱的 jQuery 小插件,将 ajax 和浏览器的 pushState API 封装到一块儿,解决了单纯使用 ajax 进行无刷新加载时对搜索引擎的不友好,而且节省了 HTTP 开支、提升了浏览速度,明显地优化了用户体验。javascript
ajax 自很少说,在这里负责携带 pjax 标识请求后端,将生成好的 html 碎片(注意不是前端取回JSON来进行渲染)取回,而后 jQuery 将它替换到 DOM 当中。php
pushState 是 html5 提供的API,是对浏览器历史对象 history
的加强。了解 Javascript 的都知道 BOM (浏览器对象模型),而 window
则是 BOM 的具体实现,history
则是window
的子对象,这个 pushState 就属于 window.history
的一个方法。简单明了。html
接下来,咱们进一步了解一下 pushState 。前端
先看下面的一段代码:html5
var stateObj = { foo: "bar" } history.pushState(stateObj, "title", "bar.html")
首先声明一个状态对象,可以储存任何可序列化数据,好比将 html 碎片存储于此,但大小有限制(640k),可使用 localStorage 等机制。固然也能够不使用,它的取舍咱们后面具体实施时会提到。java
pushState 方法往浏览器历史栈里插入一条历史项,执行完成以后,浏览器会当即将历史项中的 url(bar.html) 显示在地址栏中,(url 接受的是相对地址,会自动补上域名),但不会将其加载。而 "title" 在这里暂时没有用处,浏览器不会用它来修改页面标题,能够填 null。jquery
那什么时机调用此方法?监听你须要 pjax 效果的超连接的 click 事件,禁用默认的跳转,而后 do that。但讲到这里你可能会想到,若是用户进行浏览器的前进和后退操做,仍是会执行跳转加载,那该如何处理呢?laravel
这就要用到 pjax 不可忽视的关键角色 -- popstate 事件。这个事件只在浏览器的前进和后退操做时触发,因此经过监听它,如法炮制上述操做便可 。至此,咱们每一次的浏览访问都向搜索引擎伸出了友好的橄榄枝。 git
讲到这里,咱们大致了解了 pjax 的流程,就是监听全部须要 pjax 效果的超连接,使用 ajax 和后端达成协议取回 html 碎片并填充到 DOM 当中,pushState 负责将浏览器地址栏修改为咱们想要的 URL,而且往历史栈中增长一个历史项,经过监听 popstate,让浏览器的前进和后退也 pjax 化。github
最后,replaceState 这个 history API 也有必要介绍一下。当你须要将当前激活的历史项从历史栈中完全抹去并替换成另外一个,那用它就对了,使用方法和 pushState 彻底同样,最多见的使用场景是使用 pjax 刷新页面。
pjax 的安装配置须要先后端配合进行。先后端的轮子都有很多现成的,但前端的轮子作不到开箱即用,这是由于 pjax 的实现须要结合项目的具体代码进行实施,下面我会分别讲解。
前端采用最流行的 defunkt/jquery-pjax。话很少说,文档写的都很详细。这里主要根据源码提几点须要注意的地方:
绑定选择器时,推荐使用 data-pjax 属性,这个属性会自动寻找标签及其子标签中的超连接,绑定 click.pjax (顺便注意这里的事件命名空间,目的是为了主动 trigger 时能区别对待 click) 事件。
$(document).pjax('[data-pjax] a, a[data-pjax]', '#pjax-container')
jquery-pjax 作到了自动向后兼容,不须要单独作兼容性判断,放心调用 pjax 方法便可。
若是你的后端程序响应慢,pjax 会不耐烦的直接跳转,要么将后端程序或者网络环境优化,要么让 pjax 稍微耐心一点:
$.pjax.defaults.timeout = 1600 /\*默认 650 毫秒\*/
之因此说 pjax 不是开箱即用,主要是由于全部 js 脚本的调用会在第二次执行 pjax 方法时失效。我刚开始遇到这个问题时,一头雾水,折腾了许久而不得解,而后在 laravel china 发帖求助,很快站长龙哥就站出来,耐心细致的解答了个人疑惑。仔细研究了源码,我发现了其中两个有趣的函数:
var container = extractContainer("", xhr, options) executeScriptTags(container.scripts)
字面意思是将取回的 html 碎片 进行加工处理成一个容器对象,并处理其中的脚本标签,那为何第一次以后的 pjax 就没执行个人脚本呢?咱们继续阅读两个函数体内的关键代码:
extractContainer :
// Gather all script[src] elements obj.scripts = findAll(obj.contents, 'script[src]').remove() obj.contents = obj.contents.not(obj.scripts)
将 html 碎片中的全部带 src 的脚本删除并储存在容器对象的 scripts 属性中,将去除了 scripts 的 html 碎片内容赋给 contents 属性。看到这里,你可能会大概明白了 pjax 的用意。继续看另外一个函数体的内容:
executeScriptTags :
if (!scripts) return var existingScripts = $('script[src]') scripts.each(function() { var src = this.src var matchedScripts = existingScripts.filter(function() { return this.src === src }) if (matchedScripts.length) return var script = document.createElement('script') var type = $(this).attr('type') if (type) script.type = type script.src = $(this).attr('src') document.head.appendChild(script) })
获取目前 DOM 中的全部带 src 的脚本,而后和 html 碎片中的脚本逐个作比对,若是碎片中有新的脚本就将其插入到 head 标签的最后。啊哈~ pjax 这么作是确保不会重复请求任何已经下载过的脚本文件,节省 HTTP 开支。但这么作的弊端就是本段开头说的那个问题,那如何解决呢?
灵活运用 pjax 提供的事件。 要解决上述问题,咱们能够监听 pjax:end 事件,固然 pjax:success 和 pjax:complete 也行,区别不大 :
$(document).on('pjax:end', function() { self.blogBootUp() })
当 pjax 生命周期结束,主动调用一下脚本启动程序便可。这里,我将我全部的脚本启动程序都封装到 blogBootUp
中了,具体的代码请移步个人 Blog 项目。
使用了 pjax ,就至关于咱们阻断了浏览器的常规浏览机制,使用相关接口去重写浏览逻辑。对于浏览器的前进和后退功能,咱们监听了 popstate 事件去使用 pjax,但在常规的状况下,浏览器是有缓存的,因此咱们能秒进或秒退,但若是 pjax 不优化这一块,那前进和后退也要去请求服务器的话会付出很多的代价。查看 pjax 源码发现,做者是作了缓存处理的,经过两个关键函数 cachePush
和 cachePop
来模拟浏览器缓存,只不过是存在数组中(也就是内存中),若是你想达到 真正的 webApp 的水准,我以为还须要配合 localStorage 和 WebSocket 等相似的机制来稳健的存储数据和灵活的控制页面的缓存时间,固然这就比较复杂了,待之后再去实践好了。
后端我选用了 JacobBennett/pjax。兼容 laravel 5.* ,采用了中间件的形式,因此使用起来很简单,直接将中间件引入到 app/Http/Kernel.php 中便可。读了下源码,发现无非是使用了一个 DOM 爬虫根据客户端 header 传递的 pjax 标识和 pjax 容器标识 (就是 selector),抓取 laravel 响应对象内容中的 title 和 容器内容,而后连缀在一块儿复写回去,返回给客户端。
最后,再推荐使用一个加载效果动画的 javascript 插件,配合 pjax 使用毫无违和感。rstacruz/nprogress,文档写的很简洁明了,有专门针对 pjax 的使用说明,用上以后,仍是至关酷炫的。
原文连接:https://macken.me/article/speed-up-your-website-with-pjax