网站访问速度优化之pjax

article/2025/8/27 6:20:54

pjaxajaxpushState 的结合,它是一个 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 的参数列表,参数说明。

参数默认值说明
timeout650ajax请求如果超时将触发强制刷新
pushTRUE使用 [pushState][] 在浏览器中添加导航记录
replaceFALSE是否使用replace方式改变URL
maxCacheLength20返回的HTML片段字符串最大缓存数
version当前pjax版本
scrollTo0当页面导航切换时滚动到的位置. 如果想页面切换不做滚动重置处理,请传入false.
typeGET使用ajax的模板请求方法,参考 $.ajax
dataTypehtml模板请求时的type,参考 $.ajax
container内容替换的CSS选择器
urllink.href用于ajax请求的url,可以是字符串或者返回字符串的函数
targetlinkeventually 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:clickpjax:beforeSendpjax:startpjax:sendpjax:clickedpjax:beforeReplacepjax:successpjax:completepjax:end

如果一个 pjax 请求生命周期还未执行完,又发起一个新的 pjax 请求,原先的 pjax 请求将被取消,并执行 pjax:error

下图是发起 pjax 请求时的生命周期:

生命周期事件及其参数如下表:

事件参数说明
pjax:clickoptions链接被激活的时候触发;取消的时候阻止pjax
pjax:beforeSendxhr, options可以设置XHR头
pjax:startxhr, options
pjax:sendxhr, options
pjax:clickedoptionspjax通过链接点击已经开始之后触发
pjax:beforeReplacecontents, options从服务器端加载的HTML内容完成之后,替换当前内容之前
pjax:successdata, status, xhr, options从服务器端加载的HTML内容替换当前内容之后
pjax:timeoutxhr, options在 options.timeout 之后触发;除非被取消,否则会强制刷新页面
pjax:errorxhr, textStatus, error, optionsajax请求出错;除非被取消,否则会强制刷新页面
pjax:completexhr, options

2.2 浏览器前进后退生命周期

浏览器前进后退时不会进行 pjax 请求,一个成功的 前进后退操作会按顺序执行如下事件:pjax:popstatepjax:startpjax:beforeReplacepjax:end

生命周期事件及其参数如下表:

事件参数说明
pjax:popstatedirection 事件的属性: “back”/“forward”
pjax:startxhr(空),options内容替换之前
pjax:beforeReplacecontents, options在用缓存中的内容替换HTML之前
pjax:endxhr(空),options替换内容之后
pjax:callbackxhr(空),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 可能有一些初始化脚本的逻辑,必须等待脚本加载完成之后才可以执行初始化(我把这种加载方式称为同步加载)。但是有一些脚本又不需要初始化,可以不用等待它加载完成就执行初始化逻辑(我把这种加载方式称为异步加载)。

这里我借用了 deferasync 两个参数,如果 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:beforeReplacepjax: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")
});

http://chatgpt.dhexx.cn/article/Dxx3EPcf.shtml

相关文章

idea热更新

配置idea热更新 第一步:下载插件 JRebel idea-file-settings-plugins搜JRebel 点击installed下载 我这里已经下载好。 第二步:配置GUID 点击jrebel Activation,开始配置 第一行是服务器地址:https://jrebel.qekang.com/{GUID} G…

webpack和vue热更新

目录 webpack一些概念介绍 webpack热更新流程 1. 启动阶段 ①->②->A->B 2. 更新阶段 ①->②->③->④ vue的组件热更新模块 总结 提到热更新,首先我们要有一个概念:Vue有热更新模块,而webpack也有它的HRM模块&#x…

JAVA实现代码热更新

JAVA实现代码热更新 引言类加载器实现热更新思路多种多样的加载来源SPI服务发现机制 完整代码类加载器共享空间机制Tomcat如何实现JSP的热更新Spring反向访问用户程序类问题补充细节推荐资源 引言 本文将带领大家利用Java的类加载器加SPI服务发现机制实现一个简易的代码热更新…

cordova打包app热更新问题

定义: 基于 cordova 框架能将web应用 (js, html, css, 图片等) 打包成 App。当 App 在终端上安装后,不需要重新下载app,实现内壳更新。 原理:1.在项目根目录的config.xml文件中添加指向服务器的地址 2.在www目录中添加chcp.json配…

【热更新】游戏热更新方案

游戏热更新方案 热更新演化热更新方案【1】 进程切换1.1 利用fork、exec切换1.2 利用网关切换1.3 微服务- 进程切换注意要点 【2】 动态库替换【3】 脚本语言热更新热更新探究最简单的实现热更的方法最简单的实现热更的方法的局限性热更新全局替换模块方法的局限性 工程实现1. …

Addressable热更新

文章目录 前提配置代码实现 前提配置 (1)勾选AddressableAssetSettings设置的Disable Catalog Update On Startup选项 (2)相应的热更戏资源分组配置(注:此文采用的是动态资源更新) Can Change …

Nacos配置热更新的4种方式、读取项目配置文件的多种方式,@value,@RefreshScope,@NacosConfigurationProperties

nacos实现配置文件的热更新,服务不用重启即可读取到nacos配置中心修改发布后的最新值,spring,springboot项目读取本地配置文件的各种方式;文章中介绍了一下注解的使用:NacosConfigurationProperties,NacosP…

Unity 热更新技术 | (一) 热更新的基本概念原理及主流热更新方案介绍

🎬 博客主页:https://xiaoy.blog.csdn.net 🎥 本文由 呆呆敲代码的小Y 原创,首发于 CSDN🙉 🎄 学习专栏推荐:Unity系统学习专栏 🌲 游戏制作专栏推荐:游戏制作 &…

JAVA热更新

引言 知识储备先看这篇文章:JAVA Instrument 在这个案例中我们会利用Instrument机制实现一个简单的热更新案例。 总体来说,步骤如下: 创建一个带premain方法的jar包。这个方法定时检测某个文件然后进行热更新。命令行启动业务类时使用参数…

热更新 深度解析

APP热更新方案 为什么要做热更新 当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。 重点是还会有原来的版本遗留…

webpack热更新

什么是模热更新?有什么优点 模块热更新是webpack的一个功能,它可以使得代码修改之后,不用刷新浏览器就可以更新。 在应用过程中替换添加删出模块,无需重新加载整个页面,是高级版的自动刷新浏览器。 优点&#xff1a…

electron 热更新

1. electron自带的整体更新方式 (全量更新) 这种方式为electron官方的升级更新方式,主要是通过主进程中的autoUpdater模块进行检测升级更新的,此方式也是大家常见的大多数electron应用程序的更新方式。 检测到新版本后从服务器拉…

uniApp实现热更新

热更新 热更新是开发中常见且常用的一种软件版本控制的方式,在uniapp进行使用热更新将软件实现更新操作 思路: 服务器中存储着最新版本号,前端进行查询可以在首次进入应用时进行请求版本号进行一个匹对如果版本号一致则不提示,反之则提示进行…

Android热更新详解

一 前言介绍 正好最近又看到热更新,对以前Android 热修复核心原理:ClassLoader类加载机制做了点补充。 从16年开始开始,热修复技术开始在安卓界流行,它以classloader类加载机制为核心,可以不发布新版本就修复线上 bu…

热更新原理

对于热更新的问题就是了解两个点的问题: 如何加载补丁包,也就是如何加载dex 文件的过程(dex是补丁包,更改的文件都在补丁包中)修复后的类如何替换掉旧的类 通过这篇文章给大家介绍下我理解的热更新的逻辑&#xff0c…

Cocos Creator 3.x 热更新

前言:游戏做热更新 是基本需求; 好在 cocos-creator 已经为我们做好了方案,相对于 U3D 的热更新方案来说,使用起来很简便!,不用关注很多细节 本文使用的是 cocos-creator 3.5.2 版本 官方文档 &#xff1…

热更新原理及实践注意

首先要说明几个概念,不要混用,热部署,热加载; 热部署:就是已经运行了项目,更改之后,不需要重新tomcat,但是会清空内存,重新打包,重新解压war包运行,可能好处是一个tomcat多个项目,不必因为tomcat停止而停止…

热更新你都知道哪些?

热更新系列目录 热更新你都知道哪些?热更新Sophix的爬坑之路腾讯热更新Tinker的故事阿里热更新Sophix的故事 Android热更新 前言1. 什么是热更新?2. 主流热更新方案3. 腾讯系热更新4. 阿里系热更新总结 博客创建时间:2020.05.16 博客更新时间…

热更新技术简易原理及技术推荐

为了照顾萌新童鞋,最开始还是对热更新的概念做一个通俗易懂的介绍。 热更新用通俗的讲就是软件不通过应用商店的软件版本更新审核,直接通过应用自行下载的软件数据更新的行为。在用户下载安装App之后,打开App时遇到的即时更新,是…

热更新及其原理

热更新:是app常用的更新方式,只需下载安装更新部分的代码 工作原理:动态下开发代码,使开发者在不发布新版本的情况下修复bug和发布功能,绕开苹果审核机制,避免长时间的审核以及多次被拒绝造成的成本。 优…