英文文档地址:《Pjax》

  本人水平有限,汉化依靠机翻完成 ,如有错误,还望读者能够在评论中斧正。

  原文代码中的var关键字均被我替换为constlet


Pjax

在任何网站上轻松启用快速 AJAX 导航(通过使用 pushState() + XHR)

  Pjax 是一个独立的 JavaScript 模块,它使用AJAX(XmlHttpRequest)和pushState()来提供快速浏览的体验。

  它允许你完全改变标准网站(服务器端生成的或静态的)的用户体验,使用户感觉他们在浏览一个应用程序,特别是对于那些低带宽连接的用户,用户体验的提升会更加明显。

  不再需要完整的页面重新加载,不再需要多个 HTTP 请求。

  Pjax 不依赖其他库(包括但不限于 jQuery),它完全是用 vanilla JS 编写的。

安装

  • 你可以直接链接到该JS文件

    1
    <script src="https://fastly.jsdelivr.net/npm/pjax@VERSION/pjax.js"></script>
  • 或者其压缩版JS文件

    1
    <script src="https://fastly.jsdelivr.net/npm/pjax@VERSION/pjax.min.js"</script>
  • 你同样可以通过 NPM 安装:

    1
    npm install pjax

    注意: 如果你通过 NPM 安装,你需要从下面两种方案中选择一种:

    • 将一个脚本标签链接到pjax.jspjax.min.js,例如:

      1
      <script src="./node_modules/pjax/pjax.js"></script>
    • 使用像 Webpack 这样的 bundler。(如果没有 bundler,index.js就不能在浏览器中使用)。

  • 或者你可以克隆这个仓库,并使用 npm 通过源代码构建程序。

    1
    2
    3
    4
    git clone https://github.com/MoOx/pjax.git
    cd pjax
    npm install
    npm run build

    然后将pjax.jspjax.min.js引入到HTML中,例如:

    1
    <script src="./pjax.min.js"></script>

Pjax的功能

  在代码的封装下,它只是一个带有pushState()调用的HTTP请求。

  Pjax 使用 AJAX 加载页面,并使用pushState()更新浏览器的 URL,在这个过程中不需要重新加载你的页面布局或任何资源(JS、CSS),从而加快页面的加载。

  它适用于所有的 permalinks(永久链),可以更新页面的所有部分(包括HTML metas、标题和导航栏)。

  在不支持history.pushState()浏览器中,Pjax 不会修改任何内容。

  除此之外,Pjax 还有以下优点:

  • 不像 jQuery-Pjax 那样被限制在一个容器中
  • 支持浏览器历史记录(后退和前进按钮)
  • 支持键盘浏览
  • 对外部页面自动返回到标准导航(感谢Captain Obvious的帮助)
  • 对于没有适当 DOM 树的内部页面,自动返回到标准导航
  • 可以非常容易地添加 CSS 动画
  • 只有 6kb 左右(经压缩和 gzipped)

Pjax的工作原理

  • 它监听你想要监听的所有链接的点击(默认是所有的)
  • 当一个站内链接被点击时,Pjax 通过 AJAX 获取该页面的 HTML
  • Pjax 渲染页面的 DOM 树(不加载任何资源 - 图片、CSS、JS 等)
  • 它检查元素是否可以被替换
    • 如果页面不符合要求,就会使用标准导航
    • 如果页面符合要求,Pjax 会替换需要替换的元素
  • 然后,它使用pushState()更新浏览器的当前URL

概述

  Pjax 是全自动的,你不需要改变你现有的 HTML,你只需要指定当你的网站进行跳转时,你希望页面上的哪些元素被替换。

  参考下面的例子:

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
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html>
<head>
<!-- metas, title, styles, etc -->
<title>My Cool Blog</title>
<meta name="description" content="Welcome to My Cool Blog">
<link href="/styles.css" rel="stylesheet">
</head>

<body>
<header class="the-header">
<nav>
<a href="/" class="is-active">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>

<section class="the-content">
<h1>My Cool Blog</h1>
<p>
Thanks for stopping by!

<a href="/about">Click Here to find out more about me.</a>
</p>
</section>

<aside class="the-sidebar">
<h3>Recent Posts</h3>
<!-- sidebar content -->
</aside>

<footer class="the-footer">
&copy; My Cool Blog
</footer>

<script src="onDomReadystuff.js"></script>
<script>
// analytics
</script>
</body>
</html>

  我们希望Pjax能够拦截URL/about,并将.the-content替换为请求的结果内容。简单说就是在用户点击a标签时阻止浏览器跳转,而是通过 Pjax 来切换页面。

  因为不同页面之间的<nav>meta<aside>内容可能不同,所以如果我们能够在切换页面时更新它们那就更好了。

  总的来说,我们想在不重新加载样式或脚本的情况下,更新页面标题、meta、顶栏、内容区和侧边栏。

  Pjax 当然支持这些操作,我们可以通过告诉 Pjax 监听所有a标签的点击(这是默认的),并使用上面定义的 CSS 选择器(不要忘记最小的 meta)来轻松做到这一点:

1
2
3
4
5
6
7
8
9
const pjax = new Pjax({
selectors: [
"title",
"meta[name=description]",
".the-header",
".the-content",
".the-sidebar",
]
})

  现在,当有人使用兼容 Pjax 的浏览器浏览网页并点击站内链接时,Pjax 将接管页面的切换任务,所有需要更新的内容将被替换为在 HTML 中找到的特定内容片段。

  神奇的是!是真的! 你不需要在服务器端做任何事情!

jQuery-pjax的不同点

  • 没有 jQuery 的依赖性
  • 不局限于一个容器
  • 不需要在服务端进行适配
  • 适用于 CommonJS 环境(Webpack / Browserify)、AMD(RequireJS),甚至全局
  • 允许用 CSS 动画进行页面转换
  • 可以很容易地进行调整,因为每个方法都是公开的(因此,是可以重写的)

兼容性

  Pjax 只适用于支持history.pushState() API浏览器,当浏览器不支持该 API 时,Pjax 会进入回退模式(就是什么都不做)。

  要想知道你的浏览器是否真的支持 Pjax,可以使用Pjax.isSupported()

用法

new Pjax()

  让我们多说一说最基本的入门方法。

  在实例化 Pjax 时,你可以把设置项作为一个对象传入构造函数:

1
2
3
4
const pjax = new Pjax({
elements: "a", // 默认是:"a[href], form[action]"
selectors: ["title", ".the-header", ".the-content", ".the-sidebar"]
})

  这将在所有a标签上上启用 Pjax,并使用CSS选择器指定要替换的部分:title.the-header.the-content.the-sidebar

  在某些情况下,你可能想只针对某一些特定的元素来应用 Pjax。在这种情况下,你有两种方法:

  1. 使用一个自定义的CSS选择器(如a.js-Pjax.js-Pjax a等等)
  2. 覆盖Pjax.prototype.getElements
    注意:如果这样做,请确保返回一个NodeList。
    1
    2
    // 法一
    const pjax = new Pjax({ elements: "a.js-Pjax" })
    1
    2
    3
    4
    5
    6
    // 法二
    Pjax.prototype.getElements = function() {
    return document.getElementsByClassName(".js-Pjax")
    }

    const pjax = new Pjax()

loadUrl(href, [options])

  通过这种方法,你可以手动触发一个 URL 的跳转。(相当于没有pjax的情况下使用location.href = '...'。)

1
2
3
4
5
6
7
const pjax = new Pjax()

// 例一
pjax.loadUrl("/your-url")

// 例二 (添加设置项)
pjax.loadUrl("/your-other-url", { timeout: 10 })

handleResponse(responseText, request, href, options)

  这个方法接收原始响应,处理 URL,然后调用pjax.loadContent()将其实际加载到 DOM。

  它接受以下参数:

  • responseText (string) - 拉取到的文本,这等同于request.responseText
  • request (XMLHttpRequest) - XHR 对象
  • href (string) - 传递给loadUrl()的 URL
  • options (object) - 包含该请求的选项的对象,其结构基本上与常规的 options 对象一致,并有一些额外的内部属性

  如果您想在将数据加载到 DOM 之前处理数据,或者不想将其加载到 DOM 中,则可以覆盖此选项。

  例如,如果要检查 non-HTML 响应,可以执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
const pjax = new Pjax();

pjax._handleResponse = pjax.handleResponse;

pjax.handleResponse = function (responseText, request, href, options) {
if (request.responseText.match("<html")) {
pjax._handleResponse(responseText, request, href, options);
} else {
// 在这里处理 non-HTML 内容
}
}

refresh([el])

  使用此方法将 Pjax 绑定到 DOM 元素的子元素上,这些元素在 Pjax 被初始化时并不存在。例如,由其他库或脚本动态插入的内容。如果调用时没有传入参数,Pjax 将再次解析整个文档以寻找新插入的元素。

1
2
3
4
// 这段代码应在JS代码插入该元素后执行
const newContent = document.querySelector(".new-content");

pjax.refresh(newContent);

reload()

  强制刷新页面,等价于location.reload()

  虽然官方文档中写了“强制”一词,但是通过我个人尝试,该函数被调用时 Pjax 仍然不会重新执行 js 以及重新加载 css。

1
pjax.reload()

Options(设置项)

elements

  类型:string, 默认:"a[href], from[action]"

  这个 CSS 选择器用于帮助 Pjax 寻找要应用的链接,如果需要多个特定的选择器,请用逗号将它们分开。

1
2
3
4
// Single element
const pjax = new Pjax({
elements: ".ajax"
})
1
2
3
4
// Multiple elements
const pjax = new Pjax({
elements: ".pjax, .ajax",
})

selectors

  类型:Array, 默认:"title", ".js-Pjax"

  这个 CSS 选择器用于告知 Pjax 哪些元素在链接跳转时需要被更新。

1
2
3
4
5
6
const pjax = new Pjax({
selectors: [
"title",
"the-content",
]
})

  如果一个查询返回多个项目,它将只保留索引。

  示例:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<header class="js-Pjax">...</header>
<section class="js-Pjax">...</section>
<footer class="the-footer">...</footer>
<script>...</script>
</body>
</html>

  这个例子是正确的,应该可以“按预期”工作。

  注意: 如果当前页面和新页面的 DOM 元素数量不一样,Pjax 将回退到普通的页面加载。

switches

  类型:object, 默认:{}

  这是一个包含回调的对象,可用于用新旧元素的切换。

  对象的key应该是定义的选择器之一(来自selectors选项)。

  示例:

1
2
3
4
5
6
7
8
9
10
11
12
const pjax = new Pjax({
selectors: ["title", ".Navbar", ".js-Pjax"],
switches: {
"title": Pjax.switches.outerHTML, // 默认行为
".the-content": function (oldEl, newEl, options) {
// 这与默认行为等价
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
},
".js-Pjax": Pjax.switches.sideBySide
}
})

  回调函数中的this指向pjax对象(例如:this.onSwitch())。

内置的回调

  • Pjax.switches.outerHTML - 默认行为,使用outerHTML替换元素
  • Pjax.switches.innerHTML - 使用innerHTML替换元素并复制className
  • Pjax.switches.replaceNode - 使用replaceChild来替换元素
  • Pjax.s switches.sideBySide: - 智能替换,当你想使用CSS动画时,允许你将两个元素放在同一个父元素中。当所有的子元素的动画结束(animationend事件被触发)时,旧的元素就会被移除

编写一个回调

  你的回调函数可以做任何你想做的事情,但你必须执行下面两个行为:

  1. 以某种方式将旧元素的内容替换为新元素的内容
  2. 调用this.onSwitch()来触发附加的回调

  下面是默认的行为,我们拿他做一个例子:

1
2
3
4
const def = function(oldEl, newEl, pjaxOptions) {
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
}

switchesOptions

  类型:object, 默认:{}

  这些选项可在内容替换期间使用(通过 switches)。目前,只有Pjax.switches.sideBySide使用它。当你使用类似Animate.css的东西时,不管有没有WOW.js,这都是非常方便的。

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
const pjax = new Pjax({
selectors: ["title", ".js-Pjax"],
switches: {
".js-Pjax": Pjax.switches.sideBySide
},
switchesOptions: {
".js-Pjax": {
classNames: {
// 添加到要替换的旧元素中的class, 示例(一个淡出动画)
remove: "Animated Animated--reverse Animate--fast Animate--noDelay",
// 添加到要替代旧元素的新元素的class, 示例(一个淡入动画)
add: "Animated",
// 在返回时添加到元素上的class
backward: "Animate--slideInRight",
// 在前进时添加到元素上的class (也应用于新页面)
forward: "Animate--slideInLeft"
},
callbacks: {
// 为了在两页的同时完成一个很好的过渡
// 我们正在对要移除的元素进行绝对定位
// & 我们需要实时参数去创造一些很棒的内容
// 查看下面的相关CSS
removeElement: function (el) {
el.style.marginLeft = "-" + (el.getBoundingClientRect().width / 2) + "px"
}
}
}
}
})

  注意,remove包含Animated——reverse,这是一个简单的方法,不必有重复的转换(slideIn + reverse => slideOut)。

  下面是一个与上述配置配合良好的 css:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 注意:如果你的内容元素没有固定的宽度,在绝对定位时可能会导致问题 */
.js-Pjax { position: relative; } /* 将进行切换的父元素 */

.js-Pjax-child { width: 100%; }

/* 将被移除的元素的 position */
.js-Pjax-remove {
position: absolute;
left: 50%;
/* transform: translateX(-50%) */
/* transform不能使用,因为我们已经对移除效果使用了通用转换 (eg animate.css) */
/* margin-left: -width/2; // made with js */
/* 如果你使用自定义动画,你完全可以从switchesOptions中删除margin-left */
}

/* CSS animations */
.Animated {
animation-fill-mode: both;
animation-duration: 1s;
}

.Animated--reverse { animation-direction: reverse; }

.Animate--fast { animation-duration: .5s; }
.Animate--noDelay { animation-delay: 0s !important; }

.Animate--slideInRight { animation-name: Animation-slideInRight; }

@keyframes Animation-slideInRight {
0% {
opacity: 0;
transform: translateX(100rem);
}

100% {
transform: translateX(0);
}
}

.Animate--slideInLeft { animation-name: Animation-slideInLeft; }

@keyframes Animation-slideInLeft {
0% {
opacity: 0;
transform: translateX(-100rem);
}

100% {
transform: translateX(0);
}
}

  为了说明这个 CSS 的效果,我们提供一个 HTML 片段:

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
<!doctype html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
<section class="js-Pjax">
<div class="js-Pjax-child">
Your content here
</div>
<!--
在替换过程中,你会得到如下的 DOM 树:

<div class="js-Pjax-child js-Pjax-remove Animate...">
Your OLD content here
</div>
<div class="js-Pjax-child js-Pjax-add Animate...">
Your NEW content here
</div>

-->

</section>
<script>...</script>
</body>
</html>

history

  类型:boolean, 默认:true

  启用pushState(),禁用它将阻止 Pjax 更新浏览器历史记录。虽然可以这么做,但很少有需要这样的场景。

  在内部,这个选项是在popstate事件触发时给 Pjax 使用的(为了不再次调用pushState())。

analytics

  类型:Function|Boolean, 默认:一个推送_gap``_trackPageview或者发送ga``pageiew的函数

  允许您添加分析行为的函数。默认情况下,它会尝试使用 Google Analytics 跟踪页面视图(如果页面上存在)。每次切换页面时都会调用它,即使对于历史导航也是如此。

  设置为false可禁用此行为。

scrollTo

  类型:Integer|[Integer, Integer]|False, 默认:0

  当设置为一个整数时,这是切换页面时要滚动到的值(从页面的顶部开始,单位:px)。

  当设置为 2 个整数的数组([x, y])时,这是水平和垂直方向的滚动值。

  设置为false则禁用滚动,这将意味着页面将保持在加载新元素之前的那个位置。

scrollRestoration

  类型:Boolean, 默认:true

  当设置为true时,Pjax 将尝试在向后或向前导航时恢复滚动位置。

cacheButs

  类型:Boolean, 默认:true

  当设置为true时,Pjax 会在请求的 URL 上附加一个时间戳,以跳过浏览器的缓存。

debug

  类型:Boolean, 默认:false

  启用调试模式,这对页面调试很有用。

currentUrlFullReload

  类型:Boolean, 默认:false

  当设置为true时,点击一个指向当前 URL 的链接将触发全页面重载。

  当设置为false时,点击这样的链接将导致 Pjax 加载当前页面,而不进行全页面重载。如果你想添加一些自定义行为,可以给链接添加一个点击监听器并调用preventDefault()。这将阻止 Pjax 接收该事件。

  注意: 事件的注册必须在 Pjax 被实例化之前完成,否则 Pjax 的事件处理程序将被首先调用,而preventDefault()还没有被调用。

  下面是一些示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
const links = document.querySelectorAll(".js-Pjax");
for (var i = 0; i < links.length; i++) {
var el = links[i]
el.addEventListener("click", function (e) {
if (el.href === window.location.href.split("#")[0]) {
e.preventDefault();
console.log("Link to current page clicked");
// Custom code goes here.
}
})
}

const pjax = new Pjax()

  (注意,如果cacheBust被设置为true,检查 href 是否与当前页面的 URL 相同的代码将不起作用,这是因为附加了一个时间戳来强行破坏缓存。)

timeout

  类型:Integer, 默认:0

  XHR 请求的超时(毫秒),设置为0表示禁用超时。

事件

  无论是如何调用 Pjax,其都会触发一些事件。

  所有的事件都是从document中触发的,而不是被点击的链接。

  • pjax:send - 在 Pjax 请求开始后触发
  • pjax:complete - 在 Pjax 请求结束后触发
  • pjax:success - 在 Pjax 请求成功后触发
  • pjax:error - 在 Pjax 请求失败后触发,请求对象将作为event.options.request传递

  如果要实现加载指示,那么sendcomplete是一对很好的事件。(示例:topbar

1
2
document.addEventListener('pjax:send', topbar.show)
document.addEventListener('pjax:complete', topbar.hide)

HTTP Headers

  Pjax 在发出和接收 HTTP 请求时,会使用一些自定义的标头。如果这些请求被送到你的服务器,你可以使用这些标头来获得一些关于响应的 mate 信息。

Request Headers

  Pjax 在每个请求中都会发送以下头信息。

  • X-Requested-With: "XMLHttpRequest"
  • X-PJAX: "true"
  • X-PJAX-Selectors: 一个选择器的序列化 JSON 数组,取自options.selectors。你可以用它来只发送 Pjax 需要更新的元素,而不是发送整个页面。请注意,你需要在服务器上对其进行反序列化(例如使用JSON.parse())。

Response Headers

  Pjax 会在响应头中查询以下信息:

  • X-PJAX-URLX-XHR-Redirected-To

  Pjax 首先检查 XHR 对象上的responseURL属性,看请求是否被服务器重定向了。虽然大多数浏览器支持这个,但不是所有的。为了确保 Pjax 能够分辨出请求是否被重定向,你可以在响应中包含一个这样的 header,并设置为最终的 URL。

DOM就绪状态

  大多数时候,你会有与当前 DOM 相关的代码,需要在 DOM 准备好后再执行。

  由于 Pjax 不会在你每次加载页面时自动重新执行你之前的代码,你需要添加代码来重新触发 DOM 准备好的代码,下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
function whenDOMReady() {
// do your stuff
}

whenDOMReady()

const pjax = new Pjax()

document.addEventListener("pjax:success", whenDOMReady)

  注意:不要在whenDOMReady函数中创建pjax实例。

  如果你只想更新一个特定的部分(这是一个好主意),你可以在一个函数中添加 DOM 相关的代码,当pjax:success事件被触发时,重新执行这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
// do your global stuff
// ... DOM ready code

function whenContainerReady() {
// do your container related stuff
}

whenContainerReady()

const pjax = new Pjax()

document.addEventListener("pjax:success", whenContainerReady)

FAQ

  Q: 启用 Pjax 后 Disqus 不能正常工作,我应该如何解决这个问题?

  A: 你只需要做下面这几件事情:

  将你的 Disqus 片段包裹到一个 DOM 元素中,你将把它添加到selectors中(或者直接用class="js-Pjax"的元素包裹它),并确保在每个页面上至少有一个空的包裹器(以避免页面之间 DOM 的差异)。

  编辑你的 Disqus 代码,就像下面这样:

Pjax 执行前的片段。

1
2
3
4
5
6
7
8
9
10
11
<script>
const disqus_shortname = 'YOURSHORTNAME'
const disqus_identifier = 'PAGEID'
const disqus_url = 'PAGEURL'
const disqus_script = 'embed.js';

(function(d,s) {
s = d.createElement('script');s.async=1;s.src = '//' + disqus_shortname + '.disqus.com/'+disqus_script;
(d.getElementsByTagName('head')[0]).appendChild(s);
})(document)
</script>

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
27
28
29
<div class="js-Pjax"><!-- 这个元素需要出现在所有通过pjax加载的页面上,即使其内部为空 -->
<!-- if (some condition) { // 通过服务器端的测试来了解你是否包含这个脚本 -->
<script>
const disqus_shortname = 'YOURSHORTNAME'
const disqus_identifier = 'PAGEID'
const disqus_url = 'PAGEURL'
const disqus_script = 'embed.js'

// 如果还没有加载disqus,这里将加载disqus
if (!window.DISQUS) {
(function(d,s) {
s = d.createElement('script');s.async=1;s.src = '//' + disqus_shortname + '.disqus.com/'+disqus_script;
(d.getElementsByTagName('head')[0]).appendChild(s);
})(document)
}
// 如果disqus脚本已经加载,我们需要用正确的方式重置它
// see https://help.disqus.com/developer/using-disqus-on-ajax-sites
else {
DISQUS.reset({
reload: true,
config: function () {
this.page.identifier = disqus_identifier
this.page.url = disqus_url
}
})
}
</script>
<!-- } -->
</div>

  注意:Pjax 只运行你正在更新的容器的内联 <script> 块。

示例

  克隆这个资源库并运行npm run example,这将在你的浏览器中打开示例应用程序。

补充

  • 当你手动调用pushStatereplaceState时不要通过state来存储状态,也不要覆盖原有的state,因为 Pjax 通过这些state保证页面的正常加载。如果你通过state存储数据,Pjax 随时可能覆盖它,如果你覆盖了原有的state,可能会导致使用浏览器前进或回退功能时出现 404 错误。
  • 部分浏览器虽然支持pushState但没有完美支持(支持了,但又没有支持),现象是 Pjax 任何功能都可以正常工作,但是浏览器的url不会变化(历史记录正常)。比如说夸克浏览器就是这个样子。