英文文档地址:《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>
<!--
During the replacement process, you'll have the following tree:

<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不会变化(历史记录正常)。比如说夸克浏览器就是这个样子。