轻松丝滑 AJAX 切页体验 (Fetch + pushState) 。
Pjax 致力于提供 原生 APP 一般 的冲浪效果。抛去整页刷新,减少网路请求。不需要 jQuery 等第三方库。以纯 TS 编写,由 Babel 和 Rollup 编译和打包。
基于 MoOx/pjax 的全新版本。
🐿️ 跳转到 用法, 选项, 状态, Q&A, 或 Contributing Guide。
进入 https://www.jsdelivr.com/package/npm/@sliphua/pjax 浏览。
安装 @sliphua/pjax 包:
npm install @sliphua/pjax
克隆此仓库,然后安装:
git clone https://github.com/PaperStrike/Pjax.git
cd Pjax
npm install
每个脚本都有一个对应的 .map
文件,作为 Source Map ,用于找 BUG。浏览器不会在没开开发者工具的时候拉取它们,所以它们不会影响用户体验。更多信息,可点击链接了解。
用一个 <script>
元素链接到 pjax.js
或 pjax.min.js
,像这样:
<script src="./dist/pjax.js"></script>
导入 pjax.esm.js
或 pjax.esm.min.js
的默认值,像这样:
import Pjax from './dist/pjax.esm.js';
简单来说,一次 fetch
,一次 pushState
。
Pjax 获取新内容,更新 URL,更新页面元素,执行新内容中的脚本,然后滚动到正确的位置。避免整个页面的变动刷新。
- 侦听页面切换。
- 使用
fetch
获取目标页面。 - 使用
pushState
更新 URL。 - 使用
DOMParser
解析目标页面 DOM 树。 - 检查
selectors
选项中的各选择器在当前 DOM 和目标 DOM 中选定元素的数量是否相等。- 不相等,Pjax 使用普通切页方式,
window.location.assign
。 - 相等,Pjax 依序更新这些元素。
- 不相等,Pjax 使用普通切页方式,
- 按 DOM 次序依次执行 新载入的脚本 和 标记脚本(
scripts
)。 - 滚动到设计位置。
挑出切页时的变化区域,让 Pjax 处理其他的事务。
比如对于下面这个页面,
<!DOCTYPE html>
<html lang="">
<head>
<title>我的博客真是太酷了</title>
<meta name="description" content="来呀来呀">
<link href="/styles.css" rel="stylesheet">
</head>
<body>
<header class="header">
<nav>
<a href="/" class="is-active">主页</a>
<a href="/about">关于</a>
<a href="/contact">联系</a>
</nav>
</header>
<section class="content">
<h1>我的博客真是太酷了</h1>
<p>
常来作客,欢迎欢迎!
<a href="/about">点这里了解我</a>
</p>
</section>
<aside class="sidebar">
<h3>近期推文</h3>
<!-- 侧边栏内容 -->
</aside>
<footer class="footer">
© 我的博客真是太酷了
</footer>
<script src="onDomReady.js"></script>
</body>
</html>
我们想让 Pjax 拦截 /about
的跳转,然后把 .content
变更为新内容。
另外,我们还想替换 <nav>
以突出显示 /about
,以及更新页面 meta 描述和 <aside>
侧边栏。
总而言之我们想更新页面主标题、meta、header、内容区和侧边栏,而不想重载样式表和脚本。
我们可以通过使用下面这样的选择器来轻松实现:
const pjax = new Pjax({
selectors: [
'title',
'meta[name=description]',
'.header',
'.content',
'.sidebar',
],
});
现在,在兼容 Pjax 的浏览器里点击一个链接,上述元素就会更新使用目标链接 DOM 里的对应内容。
嗒哒!完成啦!后端不用动!
浏览器 | 兼容版本 | 发布日期 |
---|---|---|
Chrome | 66+ | Apr 17, 2018 |
Edge | 79+ | Jan 15, 2020 |
Firefox | 60+ | May 9, 2018 |
Opera | 53+ | May 10, 2018 |
Safari | 12.2+ | Jul 22, 2019 |
方法名 | 参数 | 返回类型 |
---|---|---|
Pjax.constructor | options?: Partial<Options> | Pjax |
load | requestInfo: RequestInfo, overrideOptions?: Partial<Options> | Promise<void> |
weakLoad | requestInfo: RequestInfo, overrideOptions?: Partial<Options> | Promise<void> |
switchDOM | requestInfo: RequestInfo, overrideOptions?: Partial<Options> | Promise<void> |
preparePage | switchesResult: SwitchesResult | null, overrideOptions?: Partial<Options> | Promise<void> |
Pjax.reload | / | void |
最基础的构造函数。
实例化 Pjax
时,可以使用一个对象向构造函数传递配置:
const pjax = new Pjax({
selectors: [
'title',
'.header',
'.content',
'.sidebar',
],
});
这会在所有的链接和表单上启用 Pjax,并使用 'title'
、'.header'
、'.content'
,和 '.sidebar'
CSS 选择器选择切换元素。
要禁用默认 Pjax 触发器,将 defaultTrigger
选项设为 false
。
调用此方法将中止当前 Pjax 操作,然后以 Pjax 方式切换到给定的资源。
过程中若出现中止错误 AbortError
之外的错误,Pjax 会转而使用普通切页方式 window.location.assign
。注意 AbortError
也可能在超时 timeout
时出现。
const pjax = new Pjax();
// 用例 1
pjax.load('/your-url').catch(() => {});
// 用例 2 (覆写此次调用使用的选项)
pjax.load('/your-url', { timeout: 200 }).catch(() => {});
// 用例 3 (添加后续操作)
pjax.load('/your-url')
.then(() => {
onSuccess();
})
.catch(() => {
onAbort();
});
// 用例 4 (使用设定好的 Request 对象)
const requestToSend = new Request('/your-url', {
method: 'POST',
body: 'example',
});
pjax.load(requestToSend);
// 用例 X, 多个上述括号配合
此方法行为和 load
几乎一模一样,只是对于出现的任何错误都是直接抛出。
当需要自己处理各种错误时有用。
const pjax = new Pjax();
// 用例
pjax.weakLoad('/your-url')
.then(() => {
onSuccess();
})
.catch((e) => {
onError(e);
});
此方法接收需要请求的 URL 字符串或 Request 对象。所给定请求资源的响应需包含目标 DOM 树。
它返回一个在完成下述步骤后 resolve 的 promise:
- 调用
pushState
更新页面 URL。 - 结合
switches
选项中定义的切换函数切换selectors
选项中选择的元素。 - 定义 focusCleared,如果上一步清除了页面焦点元素,定义为
true
,反之为false
。 - 以一个包含 focusCleared 的新 SwitchesResult 为参调用并等待
preparePage
。
此方法接收一个可以为 null
的 SwitchesResult。
返回一个在完成下述步骤后 resolve 的 promise:
- 如果给定的 SwitchesResult 中 focusCleared 为
true
,将页面里的第一个含有autofocus
属性的元素设为页面焦点。 - 按 DOM 次序依次执行 新载入的脚本 和 标记脚本(
scripts
)。 - 等待上述脚本中按规范会在页面初载时阻止解析器解析的脚本(例如,内联脚本、没有
async
和defer
的普通外部脚本)的执行。 - 滚动到
scrollTo
选项规定的位置。
interface SwitchesResult {
focusCleared: boolean
}
一个 window.location.reload
的简单包装,Pjax 类的静态成员。
Pjax.reload();
名称 | 类型 | 默认值 |
---|---|---|
defaultTrigger | boolean | TriggerOptions | true |
selectors | string[] | ['title', '.pjax'] |
switches | Record<string, Switch> | {} |
scripts | string | script[data-pjax] |
scrollTo | number | [number, number] | boolean | true |
scrollRestoration | boolean | true |
cache | RequestCache | 'default' |
timeout | number | 0 |
hooks | Hooks | {} |
在设为 false
或一个有 enable: false
的对象时,禁用默认 Pjax 触发器。
默认触发器拦截处理下列带来页面切换的事件:
- 指向同域链接的
<a>
或<area>
元素的触发。 - 导向同域链接的表单提交。
当页面只在某些特定时刻需要 Pjax 时,就禁用。例如,
// 将 `defaultTrigger` 设为 `false`。
const pjax = new Pjax({ defaultTrigger: false });
// 在需要时调用 `load`。
document.addEventListener('example', (event) => {
if (!needsPjax) return;
event.preventDefault();
pjax.load('/bingo');
});
使用 exclude
子选项可以只对特定元素禁用该触发器:
const pjax = new Pjax({
defaultTrigger: {
exclude: 'a[data-no-pjax]',
},
});
interface TriggerOptions {
enable?: boolean,
exclude?: string,
}
CSS 选择器列表,用于标注切页时变换的元素。例如,
const pjax = new Pjax({
selectors: [
'title',
'.content',
],
});
当一个选择器选择多个元素时,会按 DOM 次序依次替换。
每个选择器,在当前页面和新页面,选择的元素数量必须相同。否则 Pjax 会回落使用普通切页方式 window.location.assign
。
此选项存放定义新旧元素处理方式的切换函数(Switch 类型)。
对象键名应匹配 selectors
选项中定义的选择器。
举个例子:
const pjax = new Pjax({
selectors: ['title', '.Navbar', '.pjax'],
switches: {
// 默认切换函数
'title': Pjax.switches.default,
'.content': async (oldEle, newEle) => {
// 两元素的处理方式
},
'.pjax': Pjax.switches.innerText,
},
});
type Switch<T extends Element = Element> = (oldEle: T, newEle: T) => (Promise<void> | void);
返回 promise 可以让 Pjax 知道该切换函数何时结束。在所有切换函数结束后,Pjax 才会 执行新载入的、标记过的脚本这样子。
Pjax.switches.default
— 默认切换函数,与Pjax.switches.replaceWith
一致。Pjax.switches.innerHTML
— 使用Element.innerHTML
切换元素内容。Pjax.switches.textContent
— 使用Node.textContent
切换元素文本。Pjax.switches.innerText
— 使用HTMLElement.innerText
切换元素可见文本。Pjax.switches.attributes
— 只重写元素上的属性,不操作元素内容。Pjax.switches.replaceWith
— 使用ChildNode.replaceWith
切换元素。
在保证 selectors
选项中各选择器在新旧页面中选定的元素数量一致的情况下,一个切换函数干啥都行。
在下面的例子中,.current
类标记唯一的切换中的元素,所以给定的 CSS 选择器选定的元素数量不会变。在该函数返回的 promise resolve 前,Pjax 不会执行脚本元素或滚动页面。
const pjax = new Pjax({
selectors: ['.sidebar.current'],
});
const customSwitch = (oldEle, newEle) => {
oldEle.classList.remove('current');
newEle.classList.add('current');
oldEle.after(newEle);
return new Promise((resolve) => {
// 假设元素在插入 DOM 后就开始动画。
newEle.addEventListener('animationend', () => {
oldEle.remove();
resolve();
}, { once: true });
});
};
注意: Pjax 在一次切页过程中会等待切换函数的完成,但会立即处理下一个切页事件,不论当前切页完成与否。尝试在切页过程中屏蔽用户操作的变通方案往往行不通,因为用户总能使用 “返回”、“前进” 之类的按钮。
用来标记在切页过程后半段需要执行的额外 <script>
的 CSS 选择器。若需要使用多个选择器,使用英文逗号(,)分隔。使用空字符串来不标记。像这样:
// 单一选择器
const pjax = new Pjax({
scripts: 'script.pjax',
});
// 多个选择器
const pjax = new Pjax({
scripts: 'script.pjax, script.analytics',
});
// 切页时只执行新脚本
const pjax = new Pjax({
scripts: '',
});
注意: 切页时 Pjax 总会执行刷新区域载入的新脚本。不需要在这里标记它们。
若设为一个数字,此选项表示在切页后要滚动到的垂直位置。从页面顶部开始计,以 px 为单位。
若设为两个数字组成的数组 ([x, y]),此选项表示切页后要滚动到的水平和垂直位置。
设为 true
可让 Pjax 自行决定滚动位置。Pjax 会尽力表现得和浏览器默认行为一致。例如,在 hash 变化到某元素 ID 时滚动到该元素位置,在切换到一个 hash 值不为任一元素 ID 的页面时滚动到页面左上角。
设为 false
可让 Pjax 在切页时不进行任何滚动。
注意: 此选项不影响下面的滚动位置恢复行为。
在设为 true
时,Pjax 会尝试在前进、后退时恢复上次的页面滚动位置状态。
此选项控制 Pjax 请求所使用的缓存模式,与 Request.cache
的可取值及意义一致。
为 fetch 请求附加一个中止时间,以毫秒为单位。设为 0
不附加。
此选项指定一系列 Hook 钩子函数,用于更改 Pjax 中发送的请求 request、接收的响应 response、解析的文档 document 和 生成的 switchResult。下面是一个为 Pjax 请求添加自定义请求头的例子:
const pjax = new Pjax({
hooks: {
request: (request) => {
request.headers.set('My-Custom-Header', 'ready');
},
},
});
一个函数,返回值可为 undefined
、给定值的同类型值、或解析后为这二者之一的 Promise。
type Hook<T> = (input: T) => T | void | Promise<T | void>;
interface Hooks {
request?: Hook<Request>;
response?: Hook<Response>;
document?: Hook<Document>;
switchesResult?: Hook<SwitchesResult>;
}
可在 Pjax 实例上读取。
名称 | 类型 | 默认值 |
---|---|---|
location | URL | new URL(window.location.href) |
abortController | AbortController | null | null |
上一个 Pjax 认识的位置。
可中止当前 Pjax 行为的中止控制器。若 Pjax 当前空闲,null
。
例如,在某事件触发时中止 Pjax:
const pjax = new Pjax();
document.addEventListener('example', () => {
pjax.abortController?.abort();
});
在调用 Pjax 时,Pjax 可能会触发一系列事件。
这些事件都触发于 document,与所点击的链接或调用函数无关。你可以通过 event.detail
得到事件详情。
下表依序展示了各事件的触发时机:
pjax:send
事件,在 Pjax 发送请求前触发。pjax:receive
事件,在 Pjax 收到响应后触发。- Pjax 切换 DOM。
switchDOM
方法有详细描述。 - 若前面的步骤中有错误,
pjax:error
事件。 pjax:complete
事件,前面的步骤完成时触发(不论是否有错误)。- 若前面的步骤中无错误,
pjax:success
事件。
如果页面里有加载指示器 (如 topbar) ,结合 send
和 complete
事件会是不错的选择。
document.addEventListener('pjax:send', topbar.show);
document.addEventListener('pjax:complete', topbar.hide);
Pjax 在构建发送 HTTP 请求时会使用这些头数据:
X-Requested-With: Fetch
X-PJAX: true
X-PJAX-Selectors
— 由selectors
选项序列化而来的字符串,后端可以据此仅传递变化的元素,而不需要传递整个页面。一般需要使用JSON.parse
之类解析。
多数时候,页面中会有需要在 DOM 加载完成后执行的代码。
由于 Pjax 不会触发标准 DOM 加载事件,你可能需要添加一些重新触发 DOM 准备后执行函数 的代码。例如:
function whenDOMReady() {
// 干事儿
}
document.addEventListener('DOMContentLoaded', whenDOMReady);
const pjax = new Pjax();
document.addEventListener('pjax:success', whenDOMReady);
注意: 不要在 whenDOMReady
函数里实例化 Pjax。Pjax 往往只需要实例化一次。