这篇文章上次修改于 196 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

在讲述 PJAX 之前,我觉得我很有必要来说说什么是 AJAX。AJAX 的全称为 Asynchronous JavaScript And XML,也就是异步 JavaScript 和 XML。它可以让当前访问页面无需刷新即可呈现更多内容,使用了 AJAX 的网站无论是从用户体验还是速度上都比普通的网站略胜一筹。最常见的例子就是现在比较流行的瀑布流布局,在页面拉到底时浏览器会自动加载下一页的内容,而无需刷新切换页面。

PJAX 则是基于 AJAX 技术的一个新术语,也就是 PushStateAJAX。PJAX 相对于 AJAX 的区别在于当前页面在刷新的时候,浏览器会添加一个历史纪录(左上角的前进(←)与后退(→)),应用了 PJAX 技术的网站可以通过点击按钮快速回到之前的状态而依旧不需要刷新页面。从这点来看,PJAX 着实扩展了 AJAX 的功能。

那么我们该如何给自己的网站添加 PJAX 呢?开源社区给予了我们不错的解决方案,其中 JQuery 版本的 PJAX 已经被广大博客主题所采用,中文教程也比较繁多,因此不作为本文所使用的项目,在这里我就不多概述了。

可我觉得 JQuery 臃肿,有没有不需要库的 PJAX 组件呢?当然是有的,这个库非常精简,并且支持页面多个容器的批量替换,无需像 JQuery 版本必须是一个容器。在 Kico Style 推出更简洁的 PJAX 组件之前,我推荐大家使用该无 JQuery 版本。

推荐项目

MoOx/pjax【本教程的安利】
defunkt/jquery-pjax【需要 JQuery 支持】

在这里我们就以 MoOx 的项目作为教程

引用文件

你可以下载 该项目,在自己的服务器上托管代码。或者是直接使用 JSDelivr 公共 CDN 友情提供的地址:

<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.js"></script>

建议将 JS 文件放在网站的底部,防止因文件加载过慢而导致的页面阻塞打开缓慢的情况。

开始使用

你可以创建自己的 JS 文件(paul.js),用于操作 PJAX 组件。将其列在上述引用代码的下方,以确保先加载 PJAX 组件。

此处为示例引用结构,并非需要引用下面的两个文件

<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.js"></script>
<script src="https://paugram.com/static/paul.js"></script>

每个网站在刷新的过程中,总有一部分是重复的。在开始定义 PJAX 组件之前,我们首先需要分析一下那个需要添加 PJAX 的网页 DOM 结构,看看哪些元素/容器是需要被替换的。通过 Chrome 浏览器的开发工具,就可以更直观的看到它。

我们手动判断 DOM 结构,合理的编写 PJAX 替换页面内容所需要的选择器,就可以把在网页刷新过程中发生变化的那一部分给 “刷新”。在这里我将举个例子:

<!DOCTYPE html>
<html class="font-auto" lang="zh-cmn-hans">
<head>
    <meta charset="UTF-8">
    <title>保罗的小窝</title>
    <link href="https://paul.ren/static/kico.css" rel="stylesheet" type="text/css"/>
    <link href="https://paul.ren/static/paul.css" rel="stylesheet" type="text/css"/>
    <link href="https://paul.ren/static/img/icon.png" rel="icon" sizes="192x192"/>
    <meta name="viewport" content="width=device-width, maximum-scale=1, initial-scale=1"/>
    <meta name="keywords" content="奇趣保罗,鲍小螺,保罗,Dreamer-Paul,dreamer-paul,Paul,paul"/>
    <meta name="description" content="欢迎来到奇趣保罗的小窝!本站记录了他的日常与各种作品"/>
    <meta property="og:title" content="保罗的小窝"/>
    <meta property="og:author" content="Dreamer-Paul"/>
    <meta property="og:image" content="https://paul.ren/static/img/avatar.jpg"/>
    <meta property="og:site_name" content="保罗的小窝">
    <meta property="og:description" content="欢迎来到奇趣保罗的小窝!本站记录了他的日常与各种作品"/>
</head>
<body>
<header>
  页眉内容...
</header>
<main>
  正文内容...
</main>
<footer>
  页尾内容...
</footer>
</body>
</html>

只要是个网站,每次切换页面的时候,title 标签是必然得替换的。在我这个案例中,main 是每次切换页面之后会产生内容变化的容器。而另外两个容器 headerfooter 都是毫无变化的,这就无需编写了。如果想额外刷新一下 meta 标签,满足强迫症的要求(例如我)可以再加上 meta 选择器。

var pjax = new Pjax({
  // 在页面进行 PJAX 时需要被替换的元素或容器,一条一个 CSS 选择器,数组形式
  selectors: [
    "title",
    "meta[name=description]", // 如果是全部 meta 替换的话,只需要写 meta
    "main"
  ],
  cacheBust: false
})

这样,一个灰常简单的 PJAX 网站就做好了!是不是非常简单呢?

重载函数

如果你的页面内容需要配合 JS 实现一些特效(例:图片灯箱),你可能会发现刚打开页面时所执行的 JS 效果失效了。这是由于 PJAX 修改了文档 DOM 结构,当前网页的内容已经发生了变化,那些曾被函数安排过的元素已经消失。

PJAX 不像刷新页面一样,浏览器不会从头到尾分析网站,因此不会自动重新执行一次 JS。我们为了保证这些功能能正常发挥他们的作用,所以需要重新让它再运行一次,这种操作我们称之为 “重载”。像基于 Kico Style 编写的网站,图片灯箱组件就需要重新执行一次。

// 添加重载,其实就是 PJAX 完成之后的操作
document.addEventListener('pjax:complete', function (){
    // 需要重载的 JS 函数
    ks.image(":not[no-image] img"); // 重载 Kico Style 的图片灯箱
});

如果你的代码在函数体外部声明获取了一次元素(只在某些特定页面中出现的),为了保证这些元素在 PJAX 完成之后能再次生效,你需要重新声明。最好事先封装好一个类或是函数,这样就保证了代码的复用性。在 pjax:complete 事件函数中就可以直接调取它,快速实现 JS 的重载。

// 像评论这种只在某些特定页面中出现的功能,不应单独写在函数体外面声明,对 PJAX 尤其不利
var comment = document.getElementsByClassName("comment-body")[0];

使用函数封装,提升代码复用性和可读性。

function pjax_reload(){
    var comment = document.getElementsByClassName("comment-body")[0];
    ...
}

document.addEventListener('pjax:complete', function (){
    pjax_reload();
});

改善体验

在进行 PJAX 的过程中,浏览器是不会有任何加载提示的,也就是那个转圈圈的动画没有了。如果你的服务器速度比较一般的话,用户可能无法察觉到链接点击之后发生的事情,可能会认为点击没有反应。

我们可以给网站添加一个加载动画,在 PJAX 开始的时候显示它,在完成的时候隐藏它。下面这个是我现在所使用的一个案例,你也可以自己设计一个加载动画,使用图片什么的都是一样的。

HTML

<!- 添加一个加载提示 ->
<loader>
    <div class="plane"></div>
</loader>

CSS

loader{
    top: 2em;
    right: 2em;
    z-index: 3;
    opacity: 0;
    position: fixed;
    pointer-events: none;
    transition: opacity .3s;
}
loader.active{ opacity: 1 }

loader .plane{
    width: 2em;
    height: 2em;
    background: #0b63ff;
    animation: loader 1.2s infinite ease-in-out
}

@keyframes loader {
    0% {
        transform: perspective(120px) rotateX(0deg) rotateY(0deg)
    }
    50% {
        transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)
    }
    100% {
        transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg)
    }
}

实现动画的基本逻辑就是在加载中的时候使得 loader 的透明度为 1 即不透明,在加载完成的时候为 0。在这里我们配合了 Kico Style 的快速选择器进行使用。如不需要,请使用 document.querySelector() 方法进行替代。

JavaScript

// 开始 PJAX 执行的函数
document.addEventListener('pjax:send', function (){
    ks.select("loader").classList.add("active");
});

// PJAX 完成之后执行的函数,可以和上面的重载放在一起
document.addEventListener('pjax:complete', function (){
    ks.select("loader").classList.add("active");
});

这样一个改善体验的加载动画也完成了!

其他参数

history:默认值 true
是否修改历史记录,如果关闭就相当于只有 AJAX 了

timeout:默认值 0
加载超时时间

cacheBust:默认值 true
是否添加额外的时间戳,防止浏览器进行缓存,默认 true,你改成 false 的话地址栏会更加好看。

scrollRestoration:默认值 true
是否尝试设置滚动位置,将在向后或向前导航时尝试恢复滚动位置。

后端延伸

PJAX 在发送请求的时候会自带一个 X-PJAX 头,默认为 X-PJAX: "true"。你可以通过这个头在后端判断是否为 PJAX 请求从而跳过一部分内容的输出,减小服务器对部分资源的请求并实现更小的回复内容。

该组件还有一些高级的功能,例如加载时调用动画库等操作,都可以在该项目的 README 中寻找答案。

总结

通过本篇文章,我们了解到了可以快速实现 PJAX 的两个开源项目,并认识到了无 JQuery 版本的基本操作。现在你也可以尝试为自己实现一个简单的 PJAX 网站了,快来动手做一个吧!