pjax实现网站无刷新化和个人遇到的无数坑 keisawaakira

心血来潮想开篇博客,搜索教程,用jekyll在git上搭了一个。

fork并修改后,本地测试时发现随着网页的跳转,音乐的播放状态也跟着一并刷新而全部清零。

经过搜索发现了pjax这个插件,了解的人看名字就知道它跟ajax有关,它实际上是ajax+pushState的一个封装,省去了我们用ajax后再写一大段代码再pushState的麻烦。接下来教你如何用pjax实现网站无刷新化及用pjax时本人遇上的一些坑。

一、引入pjax

首先要注意的一点是,pjax是基于jquery框架编写的,因而要在引入pjax之前先引入jquery,建议全局引入。

本地引用:

在https://github.com/jquery/jquery和https://github.com/welefen/pjax中下载jquery.js与pjax.js,并上传至网站根目录中。

然后在使用pjax之前导入

  <script src='填入jquery的地址'></script>
  <script src='填入pjax的地址'></script>

网址引用:

  <script src='https://cdn.bootcdn.net/ajax/libs/jquery/1.8.3/jquery.min.js'></script>
  <script src='https://cdn.bootcdn.net/ajax/libs/jquery.pjax/2.0.1/jquery.pjax.min.js'></script>

坑:

由于pjax久未更新,最新的jquery不一定支持所下载的pjax,太旧的又没有pushState的封装,经过多种尝试,本博客采用1.8.3版的jquery与2.0.1版的pjax。

二、使用pjax

在body标签结束前插入以下代码:

  $(document).pjax('a[target!=_blank]', '要刷新的部分', {fragment:'要选取的部分', timeout:8000});

a[target!=_blank]表示网站中所有的不在新窗口打开的超链接实现pjax加载,

如果你不希望在每个地方都使用pjax,那么pjax提供了data-pjax属性让你注明需要使用pjax的链接,

<a data-pjax href='...'></a>

注明完data-pjax属性后,用’[data-pjax] a, a[data-pjax]‘代替上面的’a[target!=_blank]‘。

要刷新的部分代表本页面你想要更换内容的部分,而该部分以外的区域不进行刷新,是css选择器,用’#id名’或’.类名’选取。

fragment中要选取的部分指在超链接指向的网页中,选取这一部分替换上面的要刷新部分,一样是css选择器,如果没有这个参数,超链接指向的又是一个完整的网页(即有head有body这种网页),pjax将不工作并强制全部刷新。

timeout为设定超时的参数,如果超过了这个时间网页就强制全部刷新,默认为650,建议改大点。

坑:

一开始按网上的教程来怎么来怎么不行,尝试了好几天(是的我菜),后来发现是因为网上的教程用的全是id名选取的容器,而我使用的这个模板因为要实现一些功能不方便自由地插入脚本,所以我pjax是写在head里的,而用id名选取的话要保证脚本是插入声明完id的标签之后的(不知道是我的问题还是什么,即便设定了defer属性也不行,必须要在之后),所以运行不起来,然后看pjax的文档,发现用类名选取也可以,没有上述id名那种必须要放在之后的限制。

三、一些js插件的重载

尝试一下,网站可以无刷新加载了吧,但是再多点几下就发现,跟原来全部刷新的网站有点不同,比如prism高亮不见了等等。

是的,js插件失效了,其实不光是插件,具体说来是容器内的脚本段直接消失以及容器外绑定该容器的脚本失效。这是因为原先绑定某个标签的js插件,这个标签被pjax加载来的新的给替换掉了,而pjax的加载,并不会加载容器内的script脚本,所以在这里需要利用pjax提供的事件,执行回调函数将插件重新绑定新容器。

以本站使用的mermaid插件为例:

function mermaid_reload(){
	mermaid.init();
}
$(document).on('pjax:end', function(event){
	mermaid_reload();
});

坑:

在这儿也调试了好几天(是的我特别菜),而且这一块我踩到的坑特别多。

首先是究竟该用哪个函数才能重载js插件的问题,直接复制插件初始化函数,有的可以有的不行,比如上述的mermaid,初始化时用的是:

  mermaid.init({}, ".language-mermaid");

回调函数如果依旧执行这个,没卵用。所以对于那些不行的,最好结合插件的文档找到对应函数,然后在pjax加载js插件失败的网页里,用f12在console上执行确认有效后再写入重载函数中。

另一个坑就是插件的js文件的引入,如果该js文件在pjax容器内,一样pjax不会给你加载,那么这个js文件就需要我们再次引入,可以用以下的代码:

  $(document).on('pjax:start', function(event){
	$.getScript('插件js文件网址');
  });

但是这么做会发现在f12的source里每发生一次pjax都多一个同样的js文件,所以需要根据对应插件,判断是否引入过之后,再执行$.getScript,这是一个思路,但是我觉得一个个判断麻烦,所以干脆就:

  function js_reload(){
	var js = document.createElement('script');
	js.src = '插件js文件网址';
	js.async = 'async';
	$('你用的pjax容器')[0].insertAdjacentElement('afterbegin', js);
  }
  $(document).on('pjax:start', function(event){
	js_reload();
  });

至少这么干后,f12里显示的js文件永远只有一个,但实际上每次都进行过加载,会稍微多占用点时间,不过也没多少,于是就这样了。

最后一个坑就是,网上搜到的大部分的教程用的事件都用的是pjax:click和pjax:complete事件,而不是上面的pjax:start和pjax:end,我就这么进坑里了,后来测试发现,这会导致一个问题,这两个事件只有点击触发的pjax才会发生,如果使用浏览器的前进和后退触发的pjax,呵呵,全 部 木 大。

看pjax文档就会发现,前进或后退时一共只发生四个pjax事件(经过测试也确实只有这四个):

pjax:popstate、pjax:start、pjax:beforeReplace、pjax:end。

其中除了pjax:popstate,其他三个在点击触发的pjax里一样发生,所以上面用的是pjax:start和pjax:end。

然后测试的时候,发现自己又踩到了一个坑里。(wtf

四、进一步的优化

是的,测试的时候又发现一个坑。并不是所有的js插件经过上面的重载之后就万事大吉了,一些插件内部用到的函数会跟pjax冲突。

前面说了,pjax是ajax和pushState的封装,也就是说只要插件与ajax冲突或者对history进行操作,就有可能产生冲突,前者应该是不太常见的(至少我目前想不出来),而后者,本站一共没用多少插件,却撞上了俩,频率应该是挺高的。

先讲第一个reveal.js的例子,这是一个在网页上放ppt的插件,也就是本站首页用到的。

换页的时候如果注意上方的网址的话,你会发现它是https://keisawaakira.github.io/#/啥啥的,而不是首页链接指向的https://keisawaakira.github.io/,每换一页还在变,但网页没刷新,pjax也没触发,如果你这时候用f12并查看history的状况的话,你会发现history.state为null,这时如果点出去其他页面再用后退退回来,没用,退不回来,上方网址是变回来了,页面却不变。

这是因为pjax触发时,会用pushState帮你把history.state等给写入到你的历史记录中,而reveal插件的执行导致这个值直接为空,从而异常。

既然问题的原因找到了,那么只需要在跳转到其他页面的那一瞬间前(必须得是那一瞬间前,不然会影响插件的执行,具体为该例就是会影响ppt换页),将正确的history.state用history.replaceState写进去就行。

思路是这么个思路,但是实际执行起来不现实,因为卡不住跳转到其他页面那个瞬间(不存在这种事件,自定义事件感觉也不行,不过多半是我菜),最前最前就只有pjax:start这个事件了(因为不知道是点击还是通过前进后退,所以能用的必须要是两种情况都会发生的事件)。

但这个事件也实现不了,因为发生这个事件的时候,history.state已经跳到你要跳转的其他页面了,此时用history.replaceState,也只会替换掉正常页面的history.state。

于是先退而求其次,先限定是用点击跳转到其他页面的,那么只需要卡到pjax:beforeSend这个事件,就可以用$.pjax.state替换掉变成null的history.state,代码为:

$(document).on('pjax:beforeSend', function(event){
  if(($.pjax.state.url == 'https://keisawaakira.github.io/') || ($.pjax.state.url.indexOf('https://keisawaakira.github.io/#/') != -1)) //保证只处理出问题的首页
  {history.replaceState($.pjax.state, $.pjax.state.title, $.pjax.state.url);}
});

然后对于用前进后退跳到其他页面的,最前的pjax:popstate事件时,history也已经变化,无用。

但是我们前面不是已经解决了点击时的情况了么,那么只需要把popstate事件换成click事件,那就解决了,这是一个思路,另一个就是,既然出问题的是首页,那么只要将前进后退至首页的事件变成点击进首页,就可以了,两个区别在于前者是从首页用前进后退跳到其他页面时,后者是其他页面前进后退跳回问题页时。

本站采用后者,因为从首页出去不一定还会再通过后退后退回来,只要不再跳回问题页,不产生问题,去处理是浪费时间。

代码:

$(window).on('popstate', function(event){  //只能是popstate,因为pjax跳转后发现history.state为null就直接罢工,不发生pjax:popstate等那四个事件,这是测试出来的结论
  if(document.URL.indexOf('https://keisawaakira.github.io/#/') != -1)
  {$('首页按钮选择器').click();}
});

第二个是侧边栏里面的toc目录插件,一样,看见使用后浏览器网址上带#,具体测试也的确产生冲突,不过如果用跟上一例一样处理方法的话,会发现点击目录内索引的链接不会直接页内跳转过去,而是重新pjax加载该页面。

这是因为这个插件,也对history进行了操作,甚至还会触发popstate事件,也就是说,明明只是点击没有前进后退,却还是会触发popstate,然后执行点击函数,重新pjax加载该页面。

处理方法也很简单,该插件除了触发popstate事件以外,还触发了hashchange事件(因为是页内跳转),所以只需要将上方解决方法的事件改为hashchange即可。

五、加入进度条

对于一些较大网页的加载可能会耗费一点时间,这时如果有进度条的显示的话可以增加用户的体验,同时也可以通过迟迟不动的进度条增加用户再次点击重新发起请求的概率,以防超时而强制刷新。

相应的进度条插件可以自由选择,下面以本站用的Nprogress为例,演示下如何在pjax过程中使用进度条。

首先是全局引用:

<link href='https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.css' rel='stylesheet' />
<script src='https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.js'></script>

然后在之前的pjax:start事件中插入NProgress.start()令进度条开始:

$(document).on('pjax:start', function(event){
  NProgress.start();
  js_reload();
});

然后在pjax:end中插入NProgress.done()令进度条完成:

function mermaid_reload(){
	mermaid.init();
}
$(document).on('pjax:end', function(event){
	mermaid_reload();
	NProgress.done();
});

然后使用pjax时,就会有进度条的显示了。每个进度条插件的使用方法各不相同,建议结合相应插件文档食用。

六、最终代码示例

<html>
	<head>
		...
		<link href='https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.css' rel='stylesheet' />
		<script src='https://cdn.bootcdn.net/ajax/libs/jquery/1.8.3/jquery.min.js'></script>
		<script src='https://cdn.bootcdn.net/ajax/libs/jquery.pjax/2.0.1/jquery.pjax.min.js'></script>
		<script src='https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.js'></script>
		<script>
			$(document).pjax('a[target!=_blank]', '.container', {fragment:'.container', timeout:8000});
			function js_reload(){  //里面的网址换成你使用的插件网址
				var js1 = document.createElement('script');
				js1.src = 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js';
				js1.async = 'async';
				$('.container')[0].insertAdjacentElement('afterbegin', js1);
				var js2 = document.createElement('script');
				js2.src = 'https://cdn.jsdelivr.net/npm/reveal.js/js/reveal.min.js';
				js2.async = 'async';
				$('.container')[0].insertAdjacentElement('afterbegin', js2);
			}
			function mermaid_reload(){  //换成你使用插件的回调函数
				mermaid.init();
			}
			function ppt_load(){  //同上
				$('.container')[0].classList.add('reveal');
				let path = 'https:\/\/cdn.jsdelivr.net/npm/reveal.js/';
				Reveal.initialize({
					height: '100%',
					hash: true,
					mouseWheel: true,
					navigationMode: 'linear',
					parallaxBackgroundImage: 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAD8D+JaQAA3AA/uVqAAA=',
					dependencies: [
						{ src: path+'plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
						{ src: path+'plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
						{ src: path+'plugin/highlight/highlight.js', async: true },
						{ src: path+'plugin/zoom-js/zoom.js', async: true },
						{ src: path+'plugin/notes/notes.js', async: true },
						{ src: path+'plugin/math/math.js', async: true }
					]
				});
			}
			$(document).on('pjax:beforeSend', function(event){  //处理首页的reveal插件,若无令history.state为null的插件可删去
				if(($.pjax.state.url == 'https://keisawaakira.github.io/') || ($.pjax.state.url.indexOf('https://keisawaakira.github.io/#/') != -1))
				{history.replaceState($.pjax.state, $.pjax.state.title, $.pjax.state.url);}
			});
			$(window).on('popstate', function(event){  //同上
				if(document.URL.indexOf('https://keisawaakira.github.io/#/') != -1)
				{$('.home')[0].click();}
			});
			$(window).on('hashchange', function(event){  //处理toc插件,若无令history.state为null的插件可删去
				if((document.URL.indexOf('https://keisawaakira.github.io/tags/') != -1) && ((document.URL.indexOf('#') != -1))) 
				{history.replaceState($.pjax.state, $.pjax.state.title, $.pjax.state.url);}
				if((document.URL.indexOf('https://keisawaakira.github.io/archive/') != -1) && ((document.URL.indexOf('#') != -1))) 
				{history.replaceState($.pjax.state, $.pjax.state.title, $.pjax.state.url);}
				if((document.URL.indexOf('https://keisawaakira.github.io/_posts/') != -1) && ((document.URL.indexOf('#') != -1)))
				{history.replaceState($.pjax.state, $.pjax.state.title, $.pjax.state.url);}
			});
			$(document).on('pjax:start', function(event){
				NProgress.start();
				js_reload();
			});
			$(document).on('pjax:end', function(event){
				mermaid_reload();
				ppt_load();
				NProgress.done();
			});
		</script>
		...
	</head>
	<body>
		...
		<div class='container'>...</div>  //pjax容器,除该容器内的数据外,执行pjax时全都不刷新
		<a href='https://keisawaakira.github.io/' class='home'>...</a>  //指向首页的链接
		...
	</body>
</html>

七、一些问题

由于将popstate变成了click事件,click之后历史记录会产生更改,但由于history无法访问,所以难以修复。这时候应该考虑间接去解决的,能力有限,就先这样吧。