网站访问速度优化之pjax
pjax
是 ajax
和 pushState
的结合,它是一个 jQuery
插件。它通过 ajax
从服务器端获取 HTML 文件,在页面中用获取到的HTML替换指定容器元素中的内容。然后使用 pushState
技术更新浏览器地址栏中的当前地址,并且保持了真实的地址、网页标题,浏览器的后退(前进)按钮也可以正常使用。
由于 pjax
是局部刷新,引用的外部资源(js/css/font等)也不需要重复的加载,能够提供了极速的浏览体验。
服务器端也能够进行 pjax
支持,只提供指定需要的部分内容,从而提高响应速度。
一、发起 pjax请求
点击链接发起pjax请求:
$(document).on('click', 'a[target!=_blank]', (event) => {$.pjax.click(event, ".column-main", {scrollTo: 0,fragment: ".column-main",timeout: 8000,})
})
其中第一个参数 event
是触发 pjax
的元素;
第二个参数是要被替换内容的元素选择器;
第三个参数是 pjax
的参数列表,参数说明。
参数 | 默认值 | 说明 |
---|---|---|
timeout | 650 | ajax请求如果超时将触发强制刷新 |
push | TRUE | 使用 [pushState][] 在浏览器中添加导航记录 |
replace | FALSE | 是否使用replace方式改变URL |
maxCacheLength | 20 | 返回的HTML片段字符串最大缓存数 |
version | 当前pjax版本 | |
scrollTo | 0 | 当页面导航切换时滚动到的位置. 如果想页面切换不做滚动重置处理,请传入false. |
type | GET | 使用ajax的模板请求方法,参考 $.ajax |
dataType | html | 模板请求时的type,参考 $.ajax |
container | 内容替换的CSS选择器 | |
url | link.href | 用于ajax请求的url,可以是字符串或者返回字符串的函数 |
target | link | eventually the relatedTarget value for pjax events |
fragment | 从服务端返回的HTML字符串中子内容所在的CSS选择器,用于当服务端返回了整个HTML文档,但要求pjax局部刷新时使用。 |
除了上面那种方式外,还可以通过一下这种方式通过点击发起 pjax
请求,效果和上面的相同。
$(document).pjax("a[target!=_blank]", ".column-main", {scrollTo: 0,fragment: ".column-main",timeout: 8000,
});
除了监听点击事件外,也可以对表单进行 pjax
请求:
$(document).on('submit', 'form[data-pjax]', function (event) {$.pjax.submit(event, ".column-main", {scrollTo: 0,fragment: ".column-main",timeout: 8000,})
})
还可以使用 $.pjax.reload
重新加载当前界面。
$.pjax.reload('.column-main', options)
还可以通过 $.pjax
直接发起 pjax
请求。
$.pjax({url: url, container: '.column-main'})
二、pjax 生命周期
pjax
的生命周期方法采用事件的形式,每个事件都是异步的,即上一个事件还没有执行完,下一个事件就可以开始。
2.1 pjax请求的生命周期
一个正常的 pjax
请求将按顺序执行如下事件:pjax:click
、pjax:beforeSend
、pjax:start
、pjax:send
、pjax:clicked
、pjax:beforeReplace
、pjax:success
、pjax:complete
、pjax:end
。
如果一个 pjax
请求生命周期还未执行完,又发起一个新的 pjax
请求,原先的 pjax
请求将被取消,并执行 pjax:error
。
下图是发起 pjax
请求时的生命周期:
生命周期事件及其参数如下表:
事件 | 参数 | 说明 |
---|---|---|
pjax:click | options | 链接被激活的时候触发;取消的时候阻止pjax |
pjax:beforeSend | xhr, options | 可以设置XHR头 |
pjax:start | xhr, options | |
pjax:send | xhr, options | |
pjax:clicked | options | pjax通过链接点击已经开始之后触发 |
pjax:beforeReplace | contents, options | 从服务器端加载的HTML内容完成之后,替换当前内容之前 |
pjax:success | data, status, xhr, options | 从服务器端加载的HTML内容替换当前内容之后 |
pjax:timeout | xhr, options | 在 options.timeout 之后触发;除非被取消,否则会强制刷新页面 |
pjax:error | xhr, textStatus, error, options | ajax请求出错;除非被取消,否则会强制刷新页面 |
pjax:complete | xhr, options |
2.2 浏览器前进后退生命周期
浏览器前进后退时不会进行 pjax
请求,一个成功的 前进后退操作会按顺序执行如下事件:pjax:popstate
、pjax:start
、pjax:beforeReplace
、pjax:end
。
生命周期事件及其参数如下表:
事件 | 参数 | 说明 |
---|---|---|
pjax:popstate | direction 事件的属性: “back”/“forward” | |
pjax:start | xhr(空),options | 内容替换之前 |
pjax:beforeReplace | contents, options | 在用缓存中的内容替换HTML之前 |
pjax:end | xhr(空),options | 替换内容之后 |
pjax:callback | xhr(空),options | 页面脚本加载完成后 |
2.3 监听生命周期事件
监听 pjax:start
事件示例,其他的事件监听方法都是一样的
$(document).on("pjax:start", function (event, xhr, options) {console.log(`pjax:start`)
});
三、pjax适配方法
3.1 服务端适配
pjax 请求时将会添加请求头和请求参数信息:
x-pjax
请求头,值为true
表示当前是pjax
请求x-pjax-container
请求头,指定了当前pjax
请求的容器_pjax
url参数,指定了当前pjax
请求的目标内容
服务端可根据这些参数判断是否是 pjax
请求,以做相应的内容裁剪。
3.2 避免资源重复加载
如果将资源的引用放在 pjax
目标元素内部,那么重新加载界面时必定将导致资源文件的重新加载。
所以应该将文件放在目标元素的外部,然后在生命周期方法获取这些资源文件的引用,通过 JavaScript
判断文件是否加载,没加载的文件手动进行这些文件加载。
具体实现方法:
首先在页面加载时初始化当前已经加载到的资源文件路径信息:
const cssLoadCompletes = new Set($('link[href*=".css"]').map((i, item) => $(item).attr('href')).get())
const jsLoadCompletes = new Set($('script[src*=".js"]').map((i, item) => $(item).attr('src')).get())
经过尝试,只有在 pjax:success
事件中可以通过 data
参数拿到 pjax
请求获取到的全部的 html
信息,所以资源文件的加载只能在这个事件里进行。
以下是加载 css
文件的方法,通过 cssLoadCompletes.has(href)
判断资源文件是否已经加载过了。
data-pjax
参数是服务端的实现,服务端给特定界面才有的资源文件添加了pjax
参数,用于帮助使用link[data-pjax]
过滤。一些无论哪个界面都会加载的资源文件,如
jquery.js
他们就不可能会需要在pjax
时被加载,也就没有加data-pjax
标识。
// 将string格式的html文本初始化为元素
const $currentTarget = $($.parseHTML(data, document, true));
const $head = $("head");
$currentTarget.filter('link[data-pjax]').each(function () {let href = $(this).attr('href')if (!cssLoadCompletes.has(href)) {$head.append($(this))console.log('加载css ' + $(this).attr('href'))this.onload = function () {cssLoadCompletes.add(href)console.log('加载css完成 ' + $(this).attr('href'))}}
})
js
的加载相比较于 css
就更加的麻烦,因为我们 pjax
可能有一些初始化脚本的逻辑,必须等待脚本加载完成之后才可以执行初始化(我把这种加载方式称为同步加载)。但是有一些脚本又不需要初始化,可以不用等待它加载完成就执行初始化逻辑(我把这种加载方式称为异步加载)。
这里我借用了 defer
和 async
两个参数,如果 script
添加了这两个参数中的任意一个则将异步加载这个脚本文件,如果未添加则进行同步加载。
同样,服务端也给特定界面才有的脚本文件添加了 data-pjax
参数用于辅助过滤。
如果加载失败则不会被加入到
jsLoadCompletes
集合,下次pjax
请求时还可以重新加载。
Utils.cachedScript
是自己写个,支持读取缓存的加载方法,因为$.getScript
默认会自动添加一个时间戳参数,不能读取缓存。
let $scripts = $currentTarget.filter('script[data-pjax]');
let scriptSize = $scripts.length;
if (scriptSize > 0) {await new Promise((resolve) => {$scripts.each(function () {let src = $(this).attr('src');if (jsLoadCompletes.has(src)) {if (--scriptSize === 0) resolve()return;}if (this.defer || this.async) {console.log('异步加载js ' + src)Utils.cachedScript(src).done(function () {console.log('异步加载js完成 ' + src)jsLoadCompletes.add(src);}).fail(function () {console.log('异步加载js失败 ' + src)})if (--scriptSize === 0) resolve()} else {console.log('同步加载js ' + src)Utils.cachedScript(src).done(function () {console.log('同步加载js完成 ' + src)jsLoadCompletes.add(src);if (--scriptSize === 0) resolve()}).fail(function () {console.log('同步加载js失败 ' + src)if (--scriptSize === 0) resolve()})}})})
}
console.log('全部处理完成')
// 执行初始化逻辑
3.3 避免初始化逻辑重复执行
假如,一个 pjax
请求执行到了 pjax:success
事件,但是因为同步加载脚本速度太慢而堵塞了初始化逻辑。这时候你重新发起了一个新的 pjax
请求,然后过了一会旧请求执行了初始化逻辑,然后再过了一会,新的 pjax
也执行了初始化逻辑,界面就被初始化了两次。
这个问题可以通过给 pjax
请求添加序列号的方式进行判断,过滤旧 pjax
请求的执行。
给 pjax
请求添加序列号:
下面的代码给 pjax
请求添加了序列号,window.pjaxSerialNumber
表示全局的序列号,永远是最新一次 pjax
请求的序列号,可以全局获取。
然后本次请求将序列号加入到了请求的 options
参数中,作为 serialNumber
字段,在事件中可通过 options.serialNumber
方式获取。
const createSerialNumber = () => {const serialNumber = new Date().getTime();window.pjaxSerialNumber = serialNumber;console.log(`sn = ${serialNumber}`)return serialNumber;
}
$(document).on('click', 'a[target!=_blank]', (event) => {$.pjax.click(event, ".column-main", {scrollTo: 0,fragment: ".column-main",serialNumber: createSerialNumber(),timeout: 8000,})
})
判断当前请求是否是最新的一次请求
$(document).on("pjax:success", async function (event, data, status, xhr, options) {const serialNumber = options.serialNumber;console.log(`pjax:success sn = ${serialNumber}`)// 与window.pjaxSerialNumber比较,判断是否是最新的序列号,如果不是则直接退出if (pjaxSerialNumber !== serialNumber) return;// 这里是一堆耗时的操作console.log('全部处理完成')// 再次判断当前是不是最新的序列号if (pjaxSerialNumber !== serialNumber) return;// 进行初始化操作
});
除了在事件中判断,还应该在脚本中也进行判断,这样才能达到更细的判断粒度。
在第一次初始化界面时也应该判断 window.pjaxSerialNumber
是否为空,如果不为空则表示已经发起了 pjax
请求,一些 pjax
会重新进行的初始化操作,也不再需要在初始化时执行了,也应该停止了。
3.4 浏览器前进后退适配
以上的适配都是基于脚本加载的适配,当浏览器前进和后退时,以上的脚本必定不会被执行到,这也就表示前进和后退需要执行的那部分初始化逻辑不能放在 pjax:success
事件中。
还有,要注意的就是,浏览器前进和后退是对缓存的内容的恢复,并不是对上次 pjax
请求得到的内容的恢复。举个例子,上次 pjax
请求之后,你对部分元素进行的增删,然后进行了界面跳转。
这时,进行界面后退时就不需要再进行这些增删的操作,因为缓存的是最终的界面内容,这些修改还都是在的,但是容器外部的修改就需要重新初始化了(如导航栏页签的选中效果),因为只有容器内部的那部分内容会被恢复。
前进和后退时主要操作 pjax:beforeReplace
、pjax:end
两个事件,因为 pjax:beforeReplace
是在页面内容替换前触发的时间,pjax:end
是在页面内容替换后触发的。
pjax:beforeReplace
用于进行一些容器内容无关的操作,例如导航栏页签的选中效果。
pjax:end
用于进行容器内容相关的初始化操作,例如根据容器内容初始化文章目录。
需要注意的是,浏览器前进和后退和
pjax
请求的兼容性,因为浏览器前进后退时资源文件都是初始化好的,而pjax
请求时需要等待资源文件加载完成才能进行初始化。所以,有些初始化操作应该在
pjax
请求时在pjax:success
事件里执行,前进和后退时在pjax:end
里执行。浏览器前进后退时
xhr
参数为空,可通过判断xhr
是否为空来判断是否是浏览器前进后退。
四、示例代码
以上的适配方法都是基于开发中遇到的问题进行适配的,本文最后附上最终写好的 pjax
实现代码。
const cssLoadCompletes = new Set($('link[href*=".css"]').map((i, item) => $(item).attr('href')).get())
const jsLoadCompletes = new Set($('script[src*=".js"]').map((i, item) => $(item).attr('src')).get())// 为pjax请求创建一个序列号
const createSerialNumber = () => {const serialNumber = new Date().getTime();window.pjaxSerialNumber = serialNumber;console.log(`sn = ${serialNumber}`)return serialNumber;
}/*** 第二个参数是容器,即将被替换的内容* fragment:是加载的文本中被选中的目标内容*/
$(document).on('click', 'a[target!=_blank][href]:not(data-not-pjax)', (event) => {$.pjax.click(event, ".column-main", {scrollTo: 0,fragment: ".column-main",serialNumber: createSerialNumber(),timeout: 8000,})
})$(document).on('submit', 'form[data-pjax]', function (event) {$.pjax.submit(event, ".column-main", {scrollTo: 0,fragment: ".column-main",serialNumber: createSerialNumber(),timeout: 8000,})
})$(document).on("pjax:click", function (event, options) {console.log("------------------------")console.log(`pjax:click sn = ${options.serialNumber}`)
});$(document).on("pjax:beforeSend", function (event, xhr, options) {console.log(`pjax:beforeSend sn = ${options.serialNumber}`)
});$(document).on("pjax:start", function (event, xhr, options) {console.log(`pjax:start sn = ${options.serialNumber}`)
});$(document).on("pjax:send", function (event, xhr, options) {console.log(`pjax:send sn = ${options.serialNumber}`)// $("html, body").animate(// {// scrollTop: $("body").position().top - 60,// },// 500// );
});$(document).on("pjax:clicked", function (event, options) {console.log(`pjax:clicked sn = ${options.serialNumber}`)
});/*** pjax加载和浏览器前进后退都会触发的事件* 在此处需要进行一些未进行pjax也需要执行的程序*/
$(document).on("pjax:beforeReplace", function (event, contents, options) {console.log(`pjax:beforeReplace sn = ${options.serialNumber}`)/* 重新初始化导航条高亮 */$(".navbar-nav .current,.panel-side-menu .current").removeClass("current");commonContext.initNavbar();/* 移动端关闭抽屉弹窗 */$('html.disable-scroll').length > 0 && $('.navbar-mask').trigger("click");
});/*** pjax 替换内容成功之后* 浏览器前进后退时不会执行*/
$(document).on("pjax:success", async function (event, data, status, xhr, options) {const serialNumber = options.serialNumber;console.log(`pjax:success sn = ${serialNumber}`)if (pjaxSerialNumber !== serialNumber) return;/* 重新激活图片预览功能 */commonContext.initGallery()/* 重新加载目录和公告 */commonContext.initTocAndNotice()const $currentTarget = $($.parseHTML(data, document, true));const $head = $("head");$currentTarget.filter('link[data-pjax]').each(function () {let href = $(this).attr('href')if (!cssLoadCompletes.has(href)) {$head.append($(this))console.log('加载css ' + $(this).attr('href'))this.onload = function () {cssLoadCompletes.add(href)console.log('加载css完成 ' + $(this).attr('href'))}}})let $scripts = $currentTarget.filter('script[data-pjax]');let scriptSize = $scripts.length;if (scriptSize > 0) {await new Promise((resolve) => {$scripts.each(function () {let src = $(this).attr('src');if (jsLoadCompletes.has(src)) {if (--scriptSize === 0) resolve()return;}if (this.defer || this.async) {console.log('异步加载js ' + src)Utils.cachedScript(src).done(function () {console.log('异步加载js完成 ' + src)jsLoadCompletes.add(src);}).fail(function () {console.log('异步加载js失败 ' + src)})if (--scriptSize === 0) resolve()} else {console.log('同步加载js ' + src)Utils.cachedScript(src).done(function () {console.log('同步加载js完成 ' + src)jsLoadCompletes.add(src);if (--scriptSize === 0) resolve()}).fail(function () {console.log('同步加载js失败 ' + src)if (--scriptSize === 0) resolve()})}})})}console.log('全部处理完成')if (pjaxSerialNumber !== serialNumber) return;/* 初始化日志界面 */window.journalPjax && window.journalPjax(serialNumber);/* 初始化文章界面 */window.postPjax && window.postPjax(serialNumber);/* 加载主动推送或统计脚本 */commonContext.loadMaintain();
});$(document).on("pjax:timeout", function (event, xhr, options) {console.log(`pjax:timeout sn = ${options.serialNumber}`)
});$(document).on("pjax:error", function (event, xhr, textStatus, error, options) {console.log(`pjax:error sn = ${options.serialNumber} error ${error}`)
});// pjax结束
$(document).on("pjax:complete", function (event, xhr, textStatus, options) {console.log(`pjax:complete sn = ${options.serialNumber}`)
});/*** pjax结束,无论是pjax加载还是浏览器前进后退都会被调用* 浏览器前进后退时,唯一一个在渲染后被调用的方法*/
$(document).on("pjax:end", function (event, xhr, options) {console.log(`pjax:end sn = ${options.serialNumber}`)// 浏览器前进后退if (xhr == null) {/* 重新加载目录和公告 */commonContext.initTocAndNotice()} else if (pjaxSerialNumber !== options.serialNumber) {return;}
});$(document).on("pjax:popstate", function () {console.log("pjax:popstate")
});
网站访问速度优化之pjax相关推荐
- 浅谈网站访问速度优化
周末女朋友公司的智慧医保项目上线了,但是web端访问速度比较慢,然后就来问问我有没有好的优化方案.于是就这篇[浅谈网站访问速度优化]就诞生了. 1.备案:好多个人网站为了方便,往往不喜欢备案,就把网站 ...
- 测试网站访问速度的5个方法
网页载入速度对于一个网站来讲很关键,Google已经将一个网站的载入速度列入了网站关键字排名的考虑因素当中,也就是说如果你的网站有足够的内容,而且载入速度比别人的网站更快一步的话,那么你就是获得更好的 ...
- 百度云加速提升网站访问速度
欢迎大家进群,一起探讨学习 博主技术文档地址 博主开源微服架构前后端分离技术博客项目源码地址,欢迎各位star 今天偶然的情况,看到了百度云加速能进行网站访问速度提升,因为现在我是用学生价买的阿里云服 ...
- 提高网站访问速度的十个技巧
网站的访问速度和性能对用户体验来说是非常重要的.如果你的网站访问非常的慢,你不仅会失去用户,而且更可怕的是你会失去潜在的客户.像Google这样的互联网巨头也会把网站访问速度作为排名的一个参数. ...
- 如何让提高网站访问速度
如何让提高网站访问速度 整理方案一: 网站访问速度可以直接影响到网站的流量,而网站的访问量几乎与网站的利益直接挂钩,因此网站的速度问题成为企业及站长十分关注的问题.现在网站越来越多,不少朋友的网站打开 ...
- 天下数据教你提升网站访问速度的妙招
许多用户在建站初期出于对成本的考虑,都会选择国外虚拟主机来搭建网站,但是,一旦网页内容增多,访问量变大就会出现网站打开速度缓慢的问题.如果是搭建独立服务器.购买独立带宽带宽.CDN这类硬指标我们没有经 ...
- 网站服务器访问变慢是什么原因,网站访问速度太慢的一般解决方法
网站访问速度问题其实应该居于站长应该考虑的最优先的问题,网站访问慢直接关系到网站访问量,用户体验度的问题.今天我就来给大家介绍几种通用的解决网站太慢的方法. 1.通过浏览器插件找出网页请求过程中的加载 ...
- 服务器网页访问速度慢,分析网站访问速度慢的原因以及解决网站的访问速度
网站访问速度慢不仅会影响搜索引擎蜘蛛爬行,导致网站排名下降,而且往往会让网站的目标用户感到疯狂.关于网站开放速度对网站排名的影响,我们可以从这篇文章中了解到,"网站访问速度慢对网站排名有什么 ...
- [技术博客]使用CDN加快网站访问速度
[技术博客]使用CDN加快网站访问速度 2s : most users are willing to wait 10s : the limit for keeping the user's atten ...
最新文章
- 新手初学html日志 (一)
- 【收藏】生产订单业务流程
- C++ Boost 学习资源列表
- 搞定系统设计 01:从 0 到百万用户的系统
- Mac OS X Glut build instructions
- CrossPHP框架的常用操作
- 准备在北京Tech·Ed上组织博客园聚会
- msdia80.dll文件出现在磁盘根目录下的解决方案
- 摄影基础知识——白平衡
- Win10右下角没有英特尔显卡设置图标怎么办?
- 文化课很差能学计算机专业吗,文化成绩不好,想要学习计算机不知道能不能学呢?...
- python中patch的使用
- 泛微OA e-cology WorkflowCenterTreeData前台接口SQL注入漏洞复现
- 【Golang源码阅读】strings/builder.go
- 用java写一个算工作日期的功能(考虑到节假日以及补班的情况)
- 电脑重装系统数据恢复方法教程
- Veins源码阅读—connectionManager模块
- 【风靡全球年近40年的C++过时了吗?C++20来告诉大家】
- Android视频进阶之旅(一)_概念介绍
- 油门刹车标定表的制作