0%

2019 年 8 月,Hexo 的 NexT 主题正式加入 PJAX 功能。最初的 PR 一共包含 35 个 commit,约 600 行代码改动。不过这个数字有些夸张,其中约有 200 行是在适配 PJAX 过程中,发现一些插件对于 PJAX 不友好,顺手修改了。此后根据收到的用户反馈,又用了不下十个 PR,修复了 PJAX 中全部已知问题。

由于在同一时期,NexT 已有弃用 jQuery 的计划,因此没有采用广泛使用的 jquery-pjax,而是用了一个较为小众的库:moOx/pjax,并且根据需求修改了一些功能。
适配 PJAX 的要点总结来说有以下几条。

将 PJAX 刷新区域分离

对于一个网站中的全部网页,往往具有一些在每个页面中相同的部分,以及在每个页面中不同的部分。相同部分可能包括 <head> 中加载的 <script>,以及导航栏、页脚等组件。不同部分则是页面的正文部分等。PJAX 的刷新区域应当只包含这些不同的部分。因此,将页面中的内容进行分类非常重要。对于使用各种模版引擎生成的网站,这一操作往往并不复杂,并且还有些额外的好处,例如可以对相同的内容进行缓存,加快网站的生成时间。

选择器

一般 PJAX 会根据设定的选择器来确定刷新页面中的哪些区域。这时需要确保选择器的唯一性。例如,如果文章内容由 Markdown 渲染生成,那么可能会在各个 Heading 处产生带有 id 的元素。需要避免它们与 PJAX 刷新区域的 id 冲突,那么可以考虑使用 class 进行选择。

自洽性

另一个问题是 PJAX 刷新过程中,页面状态的自洽性。NexT 主题在适配 PJAX 时就遇到了这个问题。在侧边栏中有两个区域:文章目录和站点概览。与之对应的是两个 <button>,点击一个 <button> 便会显示其对应的区域,隐藏另一个;这一过程由 className 控制。但有一些页面,例如首页,是没有文章目录的。如果从一个文章页面通过 PJAX 刷新回到首页,就可能导致侧边栏中的两个区域都被隐藏,看上去没有内容,并且 <button> 的状态与侧栏中两个区域的状态不符合。解决方案有两种:

  • 在每次 PJAX 刷新后,根据情况「点击」其中一个按钮,确保侧边栏显示正确;
  • 将控制显示的 className 移动到刷新区域外,例如设置为 <body>className

第二种方案可以减少 DOM reflow,保证 PJAX 刷新时渲染一步到位。

重新加载脚本

一些常见的 PJAX 插件都自带了重新加载脚本的功能,但为了精确地控制,这一部分可以考虑自行实现。

页面中的脚本大致可以分为三类:

  1. 在每个页面中都存在,但只需要加载一次,重复加载反而有可能导致问题(例如音乐播放器,看板娘,背景动画等)
  2. 在每个页面中都存在,并且 PJAX 刷新时需要重新加载(例如访问量统计,FancyBox 等)
  3. 仅在部分页面中存在,不使用时没有必要加载(例如 MathJax,网站评论区等)

第一类脚本

对于第一类脚本,将其放在 PJAX 刷新区域之外即可,不需要进行任何其它的处理。这里额外提一下 addEventListener 的问题。一般的 PJAX 插件会给出 PJAX 刷新完成时的事件,需要正确的使用。而 DOMContentLoadedload 事件都只会触发一次。
此外,应当避免重复注册相同的 addEventListener。如果每个新页面都使用匿名的回调函数注册一个相同的事件,最后一个事件将触发所有的回调函数,场面一定非常壮观。要避免这个问题,可以:

  • 不使用匿名函数作为回调函数,而是将其封装,通过 function 进行声明,然后将其作为 addEventListener 的参数,这可以保证其只触发一次;
  • 或者在必要时使用 removeEventListener

第二类脚本

对于第二类脚本,需要在 PJAX 刷新完成后,重新执行。最简单的方法是复制这个脚本的属性和内容,然后将其移除,再通过 replaceChild 方法将复制插入到原来的位置。
为了使 PJAX 区分第一类和第二类脚本,避免错误地加载,NexT 使用的方法是为全部第二类脚本加上 data-pjax 属性。这样,重新加载脚本的代码如下所示

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
const elements = document.querySelectorAll('script[data-pjax]');
elements.forEach(function(element) {
const code = element.text || element.textContent || element.innerHTML || '';
const script = document.createElement('script');
if (element.id) {
script.id = element.id;
}
if (element.className) {
script.className = element.className;
}
if (element.type) {
script.type = element.type;
}
if (element.src) {
script.src = element.src;
// Force synchronous loading of peripheral JS.
script.async = false;
}
if (element.dataset.pjax !== undefined) {
script.dataset.pjax = '';
}
if (code !== '') {
script.appendChild(document.createTextNode(code));
}
element.parentNode.replaceChild(script, element);
});

复制 type 是为了避免执行非 JS 脚本,例如 MathJax 的配置。复制 dataset.pjax 则是为了确保在多次的 PJAX 刷新中,script[data-pjax] 选择器始终有效。

第三类脚本

比较麻烦的是第三类脚本。对于那些可以重复进行初始化的插件而言,其「本体」只需要加载一次,此后的 PJAX 刷新过程只需要再次进行初始化。典型的例子是 Valine 和 MathJax。对于 Valine 而言,每个新页面中,重新执行 new Valine() 即可。而对于 MathJax,初始化也是类似的。无论如何,最佳的实现应当保证:

  • 如果用户通过 PJAX 浏览的页面中,都不包含数学公式,那么无需加载 MathJax,减少网络请求;
  • 如果用户浏览到了第一个包含数学公式的页面,那么需要加载 MathJax;
  • 在此后用户浏览的所有页面中,如果包含数学公式,那么只需要调用以下方法,重新进行渲染
    1
    2
    3
    4
    MathJax.startup.document.state(0);
    MathJax.typesetClear();
    MathJax.texReset();
    MathJax.typeset();

当然,你也可以通过其它方式实现同样的效果。如果要省事的话,可以将第三类脚本按照第二类的方式进行处理。唯一的不同是需要将其放置在 PJAX 刷新区域之内,否则可能造成额外的网络请求和资源消耗。

总结

总而言之,PJAX 适配过程中最大的难点是重新加载脚本的问题。想要做到完美而没有任何疏忽和遗漏无疑是困难的。这依赖于大量的测试来检验系统的鲁棒性。

Proof by intimidation Trivial!

Proof by cumbersome notation The theorem follows immediately from the fact that when .

Proof by inaccessible literature The theorem is an easy corollary of a result proven in a hand-written note handed out during a lecture by the Yugoslavian Mathematical Society in 1973.

Proof by ghost reference The proof my be found on page 478 in a textbook which turns out to have 396 pages.

Circular argument Proposition 5.18 in [BL] is an easy corollary of Theorem 7.18 in [C], which is again based on Corollary 2.14 in [K]. This, on the other hand, is derived with reference to Proposition 5.18 in [BL].

Proof by authority My good colleague Andrew said he thought he might have come up with a proof of this a few years ago...

Internet reference For those interested, the result is shown on the web page of this book. Which unfortunately doesn't exist any more.

Proof by avoidance Chapter 3: The proof of this is delayed until Chapter 7 when we have developed the theory even further.
Chapter 7: To make things easy, we only prove it for the case , but the general case in handled in Appendix C.
Appendix C: The formal proof is beyond the scope of this book, but of course, our intuition knows this to be true.

本文转载自:Mathematicx - Facebook

你相信吗,仅仅利用一张日落的照片,你就能得出地球的半径大小!Princeton 大学的 Robert Vanderbei 在 2008 年的一篇论文《The Earth is Not Flat——Can a photo of the sunset over Lake Michigan reveal the shape of our planet ?》中对一张摄于密歇根湖的日落照片进行了分析,不但证实了地球是圆的,还依据照片上的内容对地球半径进行了估算。

事情的起因就是上面这张很平常的日落照片,以及这样一个大家平时并没有太在意的问题:太阳露出水面的部分应该是一个标准的弓形,但为什么在日出日落时,我们所看到的太阳是一个橄榄球一样的形状?大家或许会很快想到,发光体的下半部分其实是日光反射在水面上造成的。随之产生的是另一个问题:为什么它的下半部分要比上半部分小一些呢?

这是因为 —— 想到这个问题的答案并不容易 —— 地球是圆的。下图就是人站在地球上看日出的一个比例夸张版示意图,其中 O 为地球的中心,A 为人眼的位置,AB 为视平线,B 点为水天交界处。由于太阳距离我们相当遥远,因此我们把太阳光看作是一束理想的平行光线。我们把直接射入人眼的太阳光与 AB 的夹角记为,把经过水面上的一点 C 反射进入人眼的光线与 AB 的夹角记为。从图上可见,视角小,也就是说太阳在水面上的镜像比本身要小一些。

究竟比小多少呢?对照片进行精确地测量,可知太阳的直径相当于照片中的 317 个像素,而露出水面的部分高 69 像素,水中的倒影则只有 29 像素。众所周知太阳的视直径(看太阳的视角)为,因此我们就得到

如果再已知人眼(或者说相机)离水面的垂直距离 h 为 1.8 米,那么根据这些数据我们就足以估算出地球的半径了。不妨把记为,把记为,把人眼到水天相接处的距离 AB 记为 D,把人眼到反射点的距离 AC 记为 d,入射角和反射角记为,最后用 r 来表示地球半径。那么问题来了:试给出 r 的表达式并计算其值。

阅读全文 »

在 Mac 上安装了 Bootcamp 后,如果空间不足了,该如何调整大小呢?在以前的文章 MacBook 使用一块移动硬盘做 Win To Go 及 Time Machine 备份中,谈到了一种从 APFS 的主分区中划出一部分空间给 Bootcamp 的做法。但由于没有进行试验,笔者没有展开来讲。今天专门写一篇文章,记录一下几种不同的方法,以及具体的操作步骤。

Paragon CampTune

Paragon CampTune 是一个专门用来调整 Bootcamp 分区大小的商业软件。免费试用版本只能调整 2G 的空间,简直是杯水车薪。完整版则需要购买许可证。

其官方网站为:Paragon CampTune | Paragon Software

删除后重新进行分区

这是风险最小的方法:完全备份 Bootcamp 分区中的内容,然后将其移除,并在主硬盘上重新划分 Bootcamp 分区。这一方案但缺点是需要备份数据以及重装 Windows 系统,比较耗时间。

从 APFS 中划分空间给 Bootcamp

这是本文重点介绍的方法。如果你对于 Mac 的引导方式不熟悉,担心分区出现问题,那么请不要继续往下阅读,只用考虑前面的两种思路。
由于 Bootcamp 是磁盘上靠后的分区,并不能用「常规操作」向前扩充,吞并 APFS 分区的空间。这里的常规操作指的是 Mac 和 Windows 上自带的磁盘工具,其拓展卷功能只允许向后拓展磁盘分区大小。如果要吞并靠前的分区,需要借助 DiskGenius 这样的专业软件。笔者以前折腾另一台电脑上的硬盘分区时,就曾进行过类似的调整:通过 DiskGenius 使用计算器 + 手动改分区表数据,来划分各个分区的大小。这一方案同样存在分区表损坏或数据丢失的风险,开始前必须做好备份。

删除本地的 Time Machine 备份

如果在 Mac 上进行过 Time Machine 备份,请确保最新的备份已经存储在外部的硬盘上,然后执行以下命令删除本地的备份。

1
tmutil deletelocalsnapshots /

原因是 Time Machine 备份会存储在 APFS 分区的末端,如果不将它们删除,那么在进行硬盘分区时往往会提示「无法拆分此容器,因为生成的容器将太小。」

使用「磁盘工具」运行「急救」

为了让「磁盘工具」确认最大能够划分的分区大小,需要在 Macintosh HD 上运行「急救」。在「磁盘工具」的工具栏中点击「急救」,然后选择「运行」即可。

阅读全文 »