文章目录
最近给 Typecho 主题 Daydream 加上了 PJAX 无刷新。本以为加一段代码就好,结果遇到了一大堆问题,无法提交评论、插件无效、数学公式没法渲染……特此记录一下。
什么是 PJAX
pjax = pushState + ajax
对于传统的 Typecho 主题,实际上就是服务端渲染(SSR)的代表,用户每请求一个页面,服务器(php)就渲染好这个页面的内容(包括文章、评论之类的动态内容),渲染好一个静态的 HTML 然后传给浏览器。这样虽然方便,但是不难发现传输数据量其实不小。在加载不同的页面的时候,有不少部分是重复渲染的,比如页面的页眉、页脚,每个页面都是一样的,但是加载每个页面都要重新加载一次。这就会降低访问体验。
而以调用 Restful 接口等方式进行客户端渲染(CSR)的单页应用程序(SPA)则是与之对立的另一个典型。用户第一次访问只加载一个页面框架,其中的 js 代码通过接口向服务器请求数据,然后在客户端进行渲染。进入新的页面链接时只刷新需要更新的部分(一般是通过 DOM 操作)。这样每次只需要修改页面中很少的信息量,可以加快加载的速度。这称为无刷新技术。然而 Typecho 并不是以这种 CSR 的思路构建的。
(我之前开过的一个坑 Vuecho 其实就是这种尝试,这是一个 demo:alpha.skywt.cn)
那么 PJAX 实际上就是以上两者的「折中方案」。通过这段神奇的 js 代码,它可以自己判断哪些部分刷新了,然后在刷新(或者进入新的页面)时通过 DOM 操作更新要更新的部分。虽然后端的实现完全是服务器端渲染,但是前端看起来就好像是客户端渲染一样,实现全站无刷新,可以获得非常迅捷的体验。
给 Typecho 主题加上 PJAX
⚠️ 本文使用的是 jquery-pjax。
这一步其实非常简单(麻烦的在后面),
首先把博客的动态内容(即从一个页面进入另一个页面要更新的内容)用一个 <div>
之类的标签包裹起来,并将 id 设置为 pjax-container
。这个容器就叫做 PJAX 容器。
在这个容器外部,是刷新页面不需要更新的部分,如每个页面都一样的页眉、导航栏、页脚。
在这个容器内部,是刷新页面需要重新加载的部分,比如从文章列表进入一篇文章,容器中的内容由「文章列表」改变为「文章内容」。
一般主题的结构包含 header.php
、footer.php
,只要在 header.php
的末尾加上容器的开放 tag,在 footer.php
的开头加上容器的封闭 tag 即可。最后渲染出来的页面应该像是这样:
<html>
<head>
<!-- ... -->
</head>
<body>
<header><!-- 页眉,标题什么的 --></header>
<nav><!-- 导航栏什么的 --></nav>
<main id="pjax-container">
<!-- 网站主体内容,文章列表 / 文章内容 / 评论什么的 -->
</main>
<script>
// 下面要加上的代码
</script>
<footer><!-- 页脚什么的 --></footer>
</body>
</html>
在 footer.php 或者其他地方(PJAX 容器的后面,就是上面这段代码的 <script>
部分)加上这段代码:
$(document).pjax('a[href^="<?php $this->options->siteUrl()?>"]:not(a[target="_blank"])', {
container: '#pjax-container',
fragment: '#pjax-container'
});
$(document).on('pjax:send',function() {
// alert('开始加载');
// 开始加载时要运行的代码(如显示加载动画)
});
$(document).on('pjax:complete', function() {
// alert('加载完成');
// 加载完成后要运行的代码(如去除加载动画)
});
传入 pjax 函数的第一个参数是 selector,它告诉 PJAX,只对于目标为本站内的且没有设置在新页面打开(target="_blank"
)的链接进行无刷新加载,其他链接(如站外链接)则会正常加载。
第二个参数是 container,指定 id 为 pjax-container
的元素作为 PJAX 容器,当然也可以任意修改。可以查阅文档修改更多参数。
on
和 complete
都是 PJAX 事件,文档里也列举了更多事件。可以用这个测试一下 PJAX 开启成功了没有。
经过以上步骤(应该)可以发现页面之间的切换明显变快了。
解决评论问题
启用 PJAX 后,如果从一个页面进入另一个页面(比如从首页进入某个文章页面),提交评论会发现页面刷新了一下(或者 Safari 干脆显示无法加载),没法正常发表评论。必须手动刷新这个页面才能发评论。
这是因为 Typecho 提交评论的 js 脚本放在 <head>
里(F12 可以看到),而当我们刷新页面,<head>
中的脚本并不会更新(因为其在 PJAX 容器之外)。所以只有第一次进入页面可以正常提交评论,进入其他页面后就不行。
解决方法也很简单。在评论区加上这段从 <head>
里 copy 出来的代码:
(function() {
window.TypechoComment = {
dom: function(id) {
return document.getElementById(id);
},
create: function(tag, attr) {
var el = document.createElement(tag);
for (var key in attr) {
el.setAttribute(key, attr[key]);
}
return el;
},
reply: function(cid, coid) {
var comment = this.dom(cid),
parent = comment.parentNode,
response = this.dom('<?php echo $this->respondId; ?>'),
input = this.dom('comment-parent'),
form = 'form' == response.tagName ? response : response.getElementsByTagName('form')[0],
textarea = response.getElementsByTagName('textarea')[0];
if (null == input) {
input = this.create('input', {
'type': 'hidden',
'name': 'parent',
'id': 'comment-parent'
});
form.appendChild(input);
}
input.setAttribute('value', coid);
if (null == this.dom('comment-form-place-holder')) {
var holder = this.create('div', {
'id': 'comment-form-place-holder'
});
response.parentNode.insertBefore(holder, response);
}
comment.appendChild(response);
this.dom('cancel-comment-reply-link').style.display = '';
if (null != textarea && 'text' == textarea.name) {
textarea.focus();
}
return false;
},
cancelReply: function() {
var response = this.dom('<?php echo $this->respondId; ?>'),
holder = this.dom('comment-form-place-holder'),
input = this.dom('comment-parent');
if (null != input) {
input.parentNode.removeChild(input);
}
if (null == holder) {
return true;
}
this.dom('cancel-comment-reply-link').style.display = 'none';
holder.parentNode.insertBefore(response, holder);
return false;
}
};
})();
注意其中的 <?php echo $this->respondId; ?>
,这就是前文评论失败的原因——每个页面的 respondId
不一样,所以这个是需要刷新的。
除此之外,需要关闭 Typecho 后台的「设置」-「评论」-「评论提交」中取消勾选「开启反垃圾保护」。这个选项实际上是开启反 CSRF 攻击的防护,即每次加载页面服务器会传一个每次不同的随机字符串(在页面里表单最后面一个名为 _
的 hidden input 元素),提交评论时表单里会带上它,而用 PJAX 的方式去拿这个字符串非常麻烦,索性关闭了。最好加上一些反垃圾评论的插件,因为没有了反 CSRF 防护,可以用脚本轻易多次提交评论。
经过以上的修改,应该可以正常提交评论了。
解决插件问题
KaTeX 数学公式没法渲染(第一次加载能渲染,进入新页面没法渲染),也是和评论类似的问题。渲染 KaTeX 公式的这段 js 代码:
renderMathInElement(
document.body,
{
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "$", right: "$", display: false},
]
}
);
放在 PJAX 容器之外,所以只会在最初加载页面的时候执行一次。它涉及到 DOM 操作,我们希望它每次都能执行。解决方法是放在 PJAX 容器内部,或者在 complete
事件中也执行一次。
除此之外,包括 aplayer 插件、fancybox 图片灯箱、代码高亮等等都是类似的问题,只要把 DOM 操作的代码放到 PJAX 容器内部即可。
经过以上的操作,基本上网站的 PJAX 就没问题啦。享受极速的响应吧~