对博客部署 Pjax 的好处在于局部重载时可以保留一些元素(如音乐播放器)不被刷新去除掉。然后是关于 Pjax 的版本选择,最早一版为 defunkt/jquery-pjax,需要依赖 jQuery 框架,上一次更新已经是三年前了。所以本博客在挑选框架时,使用的是另一个版本: MoOx/pjax。这版去除了 jQuery 依赖,当然这不是重点,主要是更新的挺勤,我喜新厌旧。

  咳咳,言归正传,我在查阅文档时没有翻到太多的资料,所以这部分我准备认真写下,慢慢更新,不过本主题中涉及到 Pjax 部署的代码可在 部署 Pjax 这次提交记录中查看到,其实不同主题有着不同的处理方法,不能一概而论,这里也算是抛砖引玉吧。大致罗列一下对主题的修改:

  • 重新引入音乐插件,修复播放器。
  • 去处原主题中 fancybox 插件,重新单独引入。
  • 去除原主题中导航栏下加的载条动画,引入 nprogress。
  • 删除各个独立模板中的页脚部分,将其提取公共样式文件中。
  • 删除主题中的:封面、除了 algolia|hexo 之外的搜索、代码块复制的代码。

一、Pjax 简介

不负责任的复制粘贴 呱 🐸 ~

Easily enable fast AJAX navigation on any website (using pushState() + XHR)

Pjax is a standalone JavaScript module that uses AJAX (XmlHttpRequest) and pushState() to deliver a fast browsing experience.It allows you to completely transform the user experience of standard websites (server-side generated or static ones) to make users feel like they are browsing an app, especially for those with low bandwidth connections.

No more full page reloads. No more multiple HTTP requests.

Pjax does not rely on other libraries, like jQuery or similar. It is written entirely in vanilla JS.

二、前期准备

2.1 分析网站布局

  对于 Hexo 这类静态博客来说,网站内容是根据模板文件生成的,所以其中存在着大量共有的元素。以本主题为例,可以划分成四部分:导航栏文章部分侧边栏页脚。很明显,各个页面中相似的元素是 导航栏页脚 ,那么对应到主题布局上,这部分是由 layout.ejs 文件控制的。我们找到核心部分,去除掉无用的干扰项后分析一番:

一个简单的页面分析
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<%- partial('_partial/head') %> // 加载 head 标签
<body>
<%- partial('_partial/cover', {showCover: showCover}) %> // 加载导航栏
<div class="l_body<%- showCover ? '' : ' nocover' %>"> // 加载封面
<div class='body-wrapper'>
<%- body %> // 页面内容主体
</div>
</div>
<%- partial('_partial/scripts') %> // 引入 js 文件
</body>
</html>

  由此可知,页面中变动的部分都位于 <%- body %> 这个标签中了,那么它就是目标了,在 class='body-wrapper' 后面添加 id="pjax-container" 以备后用。

2.2 Pjax 的配置项

  详细的文档可在项目的 readme 中查看,传送链接:Pjax Document

  在正式使用 Pjax 之前,需要先添加它的 js 文件,位置上没那么讲究,但建议放在 scripts.ejs 的首行。Pjax 的初始化写法与 jquery-pjax 不完全相同,本博中的初始化函数是这样的:

Pjax 初始化函数
1
2
3
4
var pjax = new Pjax({
elements: 'a[href]:not([href^="#"]):not([href="javascript:void(0)"])',
selectors: ["#pjax-container","title","meta",".nav-sub.container.container--flex"]
});

  这里共配置了两部分,选择器(selectors)元素 (elements) 。默认情况下 pjax 处理的元素为 a[href], form[action] ,但是并不是所有的 <a> 标签都可追踪,所以使用 :not 语法排除一些不需要使用 pjax 跳转的元素;接着是选择器(class 或 id 选择都行),选择器用以选定重载的范围,个人理解为在 指定标签内的内容,在跳转页面时均被替换,不在这个范围内的不做处理。

  对于本博客,一些按钮的点击无需处理,所以忽略掉;重载页面时,除了上文中提到的需要重载区域外,还需要重载掉 head 中的 title meta 等标签(其实这个 meta 的范围可以进一步指定),除此之外,还附加了一个重载区域:文章导航栏,这个原因最后说。一般情况下,在初始化之后,Pjax 就已经在工作了,此时点击页面上的链接就可以看到重载页面。但是重载局部页面还带来了另外一些影响,原来绑定的元素因为内容变动的缘故,都会失效,所以这里还有另外三个有用的 Pjax 函数:

比较有用的 Pjax 函数
1
2
3
pjax.loadUrl();
document.addEventListener('pjax:send', function () {});
document.addEventListener('pjax:complete', function () {});

  他们的作用分别是:pjax 事件,监听 pjax 请求开始后和请求完成后时的事件、单独发送 pjax 请求。比如加载动画可以在 send 事件中开始,在 complete 事件中结束,再比如重新绑定的函数都可以放在完成事件里。而单独发送 pjax 请求,这里用在了搜索结果上,搜索结果是后期渲染进去的,并未被 pjax 追踪,所以采用追加点击事件,单独发起 pjax 请求的处理方法。

2.3 局部重载:Javascript

  首先,是一些需要每次进入页面,都必须重新加载的 JS 文件,典型的有不蒜子网页计数、各类分析脚本,自然而然他们必须要多次加载,对于这类的操作,简单的处理方案就是在引入相关 js 文件时,加入格外的属性,如 data-pjax ,统一处理具有这个属性的 JS 文件,在跳转页面时重新导入(以不蒜子为例子):

JS 文件整体重载
1
2
3
4
5
6
7
8
<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" data-pjax></script>
<script>
document.addEventListener('pjax:complete', function () {
$('script[data-pjax], .pjax-reload script').each(function () {
$(this).parent().append($(this).remove());
});
});
</script>

  另一种情况是,引入的 JS 文件无需重复导入,但是绑定的函数需要重新处理,比如 Fancybox 这类弹窗,这类的可以写在函数里,在页面加载完成后 $(document).ready(function(){}) 和 Pjax 重载完成后重新调用,比如本站的 Fancybox 初始化函数:

Js 函数重载(以 Fancybox 为例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
function pjax_fancybox() {
$('article img').not('[hidden]').not('.fancybox-fix img').not('.vemojis img').not('.avatar').each(function() {
var $image = $(this);
var imageCaption = $image.attr('alt');
var $imageWrapLink = $image.parent('a');
if ($imageWrapLink.length < 1) {
var src = this.getAttribute('src');
var idx = src.lastIndexOf('?');
if (idx != -1) {
src = src.substring(0, idx);
}
$imageWrapLink = $image.wrap('<a href="' + src + '" style="display:block;text-align: center;"></a>').parent('a');
}
$imageWrapLink.attr('data-fancybox', 'images');
if (imageCaption) {
$imageWrapLink.attr('data-caption', imageCaption);
}
});
$().fancybox({
selector: '[data-fancybox="images"]',
hash: false,
loop: false,
buttons: [
"zoom",
"close"
]
});
};
</script>

  Fancybox 在每一页重新绑定,同类的代码都可以采用以上的处理方式。需要重新加载的整个 JS 文件的就单独重载,需要重新执行绑定函数的,就在 Pjax 的事件监听函数中重新调用。

2.4 文章独有的变量

  这里的变量是指那种写在头部的变量,他们在页面上不存在(发挥在 Hexo 的渲染阶段)。典型例子为本主题的评论部分的配置,这部分基本位于 scripts.ejs 文件中,当读取相应的配置属性时,从主题配置文件读取的也还好,毕竟无需在意变动的问题,麻烦就麻烦在从文章页面中读取的属性。

  比如评论的占位符和地址的自定义,渲染阶段所产生的结果在页面经过 Pjax 局部重载后拿不到。所以这里换个思路,将一些变量藏在文章区域内,在使用时通过元素选择器调用。

三、兼容修改

  好了,前文铺垫结束,下面记录一些本主题特有的修改啊喵 🎉。

3.1 导航栏

   导航栏不位于重载区域内的,按理也该属于不变的的元素,但是在当前主题中,在文章页面下往下滑动时,有一个功能是导航栏切换成文章标题。问题便出现于此,除此之外,更糟糕的是在手机端的布局下,文章导航栏还同时肩负了目录开关的功能,手机端的兼容还比 PC 端下还要复杂。

   这里,进一步步分析得知这个上滑、下滑切换过程,是通过监听 scroll 事件,来判断滚动趋势进而切换导航栏。所以首要工作便是,Pjax 重载页面后,完成导航栏的切换,且只能发生在文章页面下(原主题没有这方面的处理)。

未完待续!

四、后记

4.1 参考链接

吃水不忘挖井人,除 Pjax 的官方网站外,另外两个站点也对我的帮助很大,这里予以记录:

4.2 本页面的独特修改

   因为文章的代码不出预料的会很多,所以萌生了使用折叠框的念头,在不改动渲染器的情况下,采取了直接写 html 代码的思路。但是未曾想到折叠框在伸缩、展开的过程中,因改变了页面高度的缘故影响到目录导航栏的激活跟随,思索了一阵子后没发现较好的解决办法,特此记录,代办代办(有想法的小伙伴也可以留言啊,或者有更好的代码块折叠方案什么的)。

   最主要的是需要解决高度的获取:elem.getAttribute('href')).offset().top

相关代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function setTocToggle() {
//这是一个妥协修改,我尝试利用 html 的 <details> 标签在页面中实现折叠效果
//但是折叠的过程中,很明显的是页面高度发生了变化,所以带来的影响就是下面的
//计算部分将出现错误的结果,我没有找到更好的解决办法,于是选择了在这类文章
//中取消 TOC 的高亮。 TODO:fix it
const check = $.trim($('#fixDetailsToc').text());
if(check === "true") return;

let liElements = Array.from($toc.find('li a'));
let getAnchor = () => liElements.map(elem => Math.floor($(elem.getAttribute('href')).offset().top - scrollCorrection));

let anchor = getAnchor(); // 记录的是各个 TOC 的高度
let scrollListener = () => {
let scrollTop = $('html').scrollTop() || $('body').scrollTop();
if (!anchor) return;
let l = 0,
r = anchor.length - 1,
mid;
while (l < r) {
mid = (l + r + 1) >> 1;
if (anchor[mid] === scrollTop) l = r = mid;
else if (anchor[mid] < scrollTop) l = mid;
else r = mid - 1;
}
$(liElements).removeClass('active').eq(l).addClass('active');
};

4.3 Mathjax 兼容性测试

记多项式 G(x)=i=0n1F(ωni)xiG(x)=\sum\limits_{i=0}^{n-1}F(\omega_n^i)x^i ,考虑对 G(x)G(x)FFTFFT,得到:

G(ωnk)=i=0n1F(ωni)(ωnk)i=i=0n1(j=0n1fj(ωni)j)(ωnk)i=j=0n1fji=0n1(ωnj+k)i\begin{aligned}G(\omega_n^k)&=\sum\limits_{i=0}^{n-1}F(\omega_n^i)(\omega_n^k)^i\\\\&=\sum\limits_{i=0}^{n-1}\left(\sum\limits_{j=0}^{n-1} f_j(\omega_n^i)^j\right)(\omega_n^k)^i\\\\&=\sum\limits_{j=0}^{n-1}f_j\sum\limits_{i=0}^{n-1}(\omega_n^{j+k})^i\end{aligned}

发现只有当 j+k0(modn)j+k\equiv 0 \pmod ni=0n1(ωnj+k)i=n\sum\limits_{i=0}^{n-1}(\omega_n^{j+k})^i=n ,其他情况都为 00 ,于是:

G(ωnk)=j=0n1fji=0n1(ωnj+k)i=f(nk)modnn\begin{aligned}G(\omega_n^k)&=\sum\limits_{j=0}^{n-1}f_j\sum\limits_{i=0}^{n-1}(\omega_n^{j+k})^i\\\\&=f_{(n-k)\bmod n}\cdot n\end{aligned}

摘自 AutumnKite’s Blog —— 《多项式相关


 评论