一、 Pjax 局部刷新原理

在进入正文之前,我们先简单的了解:单页面应用(英语:single-page application,缩写 SPA)是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面(多页面应用)。这种方法避免了页面之间切换打断用户体验,使应用程序更像一个桌面应用程序。与单页应用的交互通常涉及到与网页服务器后端的动态通信。说白了也就是通过 pushState() + XHR 技术实现的页面切换。

目前主流的前端框架 Vue 与 React 都是单页面应用

而很多博客站点都是 PHP 服务端渲染的多页面应用,所以为了优化体验这里便引出了本文的主角Pjax了。其思路是通过拦截<a><form>等标签的默认行为,发送 XHR 请求获取到目标页面再截取目标页面的指定区域内容与当前页面的指定区域内容进行替换。由于 Pajx 再请求过程中获取的是整个页面的 HTML 内容,所以是无法达到加速的,但是可以减少页面中的 Js 文件和图片等媒体资源的重复请求,此外还可以利用一些预加载技术(预读缓存)和磁盘缓存进一步提升访问速度,实际体验效果是极佳的。

二、 前期的准备工作

正如前文中所说,再一次 Pjax 请求获取完整的 HTML 过程中,从获取到的结果中找到选中的内容替换到页面里,所以我们需要先划分页面结构,确定被替换的区域。

2.1 分析网站页面布局

一个网站的内容有什么?对于一般博客来说,网站内容是根据模板文件生成的,其中存在着大量共有的元素。大概可以划分成这几部分:导航栏、文章部分、侧边栏、页脚。在这些区域中,潜在的重复内容是导航栏页脚,一般来说文章区域和侧边栏区域是不会相同的,举个简单的例子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>例子页面</title>
  </head>
  <body>
    <header>这里是页头(导航栏)内容...</header>
    <div id="main">这里是文章主体内容...</div>
    <div id="secondary">这里是侧边栏内容...</div>
    <footer>这里是页脚内容...</footer>
  </body>
</html>

观察例子中的代码,页面中变动的部分位于
<div id="main">这里是文章主体内容...</div>
<div id="secondary">这里是侧边栏内容...</div>
那么它们就是目标了,例子中我们已经给他们加入了各自的 id 以方便选中元素。

2.2 Pjax 的配置项

在这里,我们用的是 MoOx 版本的 Pjax,详细的文档可在项目的 readme 中查看,传送链接:Pjax Document
在正式使用 Pjax 之前,需要引入它的 js 文件,(JSDelivr 公共 CDN 友情提供的地址)
<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script>
建议将 JS 文件放在网站的底部,防止因文件加载过慢而导致的页面阻塞打开缓慢的情况。

Pjax 的初始化写法:

var pjax = new Pjax({
  elements: [
    'a[href]:not([href^="#"]):not([href="javascript:void(0)"])', // 拦截正常带链接的 a 标签
  ],
  selectors: ["#pjax-container", "title"], // 根据实际需要确认重载区域
});

这里共配置了两部分,选择器(selectors)元素 (elements) 。默认情况下 pjax 处理的元素为 a[href], form[action] ,但是并不是所有的 <a> 标签都可追踪,所以使用 :not 语法排除一些不需要使用 pjax 跳转的元素;接着是选择器(class 或 id 选择都行),选择器用以选定重载的范围,个人理解为在 指定标签内的内容,在跳转页面时均被替换,不在这个范围内的不做处理。

除此之外,我们还有三个相对重要的 Pjax 事件函数可以使用:

document.addEventListener("pjax:send", function () {});
document.addEventListener("pjax:complete", function () {});
document.addEventListener("pjax:error", function () {});

其他参数:

  • history:默认值 true 是否修改历史记录,如果关闭就相当于只有 AJAX 了
  • timeout:默认值 0 加载超时时间
  • cacheBust:默认值 true 是否添加额外的时间戳,防止浏览器进行缓存,默认 true,你改成 false 的话地址栏会更加好看。
  • scrollRestoration:默认值 true 是否尝试设置滚动位置,将在向后或向前导航时尝试恢复滚动位置。

2.3 一些潜在的问题

至此,我们已经完成了引入 Pjax 文件,划定重载区域,初始化 Pjax 对象,是不是一切就高枕无忧了呢?当然,答案是否定的。这里可以引用一句古老的名言 ”大人,时代变了!”,当下的网站早已不是个孤零零的产物,而是和 Javascript 有着密切联系后的动态页面,存在着大量的事件监听处理。不巧的是在替换内容时,部分事件监听丢失了,异常、错误、功能失效等等,就愉快的上演了。

Javascript 部分自然是需要重新绑定注册的,但是是不是无脑全部重新绑定就行呢?答案也是否定的,我们会有个大敌:重复事件监听,它或许不会立即爆发危害,但是随着浏览页面的增加,事件绑定可能会越来越累加,影响效率,潜在造成错误等等。所以这里就需要利用 send 和 complete 这对事件了,合理利用,正确解决问题。

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

三、后期的兼容处理

3.1 几种兼容思路

首先,是一些需要每次进入页面,都必须重新加载的 JS 文件,典型的有网页计数、各类分析脚本,自然而然他们必须要多次加载,对于这类的操作,简单的处理方案就是在引入相关 js 文件时,加入格外的属性,如 data-pjax ,统一处理具有这个属性的 JS 文件,在跳转页面时重新导入:

<script async src="//xxx.xxx/xxx.js" data-pjax></script>
<script>
  document.addEventListener("pjax:complete", function () {
    $("script[data-pjax], .pjax-reload script").each(function () {
      $(this).parent().append($(this).remove());
    });
  });
</script>

另一种情况是,引入的 JS 文件无需重复导入,但是绑定的函数需要重新处理,这类的可以写在函数里,在页面加载完成后 $(document).ready(function(){}) 和 Pjax 重载完成后重新调用,比如:

<script>
  function reload() {
    // 重载函数
  }
  // 页面初始化完成调用重载函数
  $(document).ready(function () {
    reload();
  });
  // pjax重载页面完成后调用重载函数
  document.addEventListener("pjax:complete", function () {
    reload();
  });
</script>

3.2 改善体验

为了解决加载提示的问题可以给网站添加一个加载动画,在 Pjax 开始的时候显示它,在完成的时候隐藏它。这里推荐一个现成的配套加载进度条库。NProgress
NProgress 的使用方式非常简单,只需引入 css 和 js 文件即可。 需要使用的 api 也就两个:

NProgress.start(); // 开始进度条
NProgress.done(); // 结束进度条

// Pjax发送请求开始时打开进度条
document.addEventListener("pjax:send", function () {
  NProgress.start();
});

// Pjax发送请求成功时关闭进度条
document.addEventListener("pjax:complete", function () {
  NProgress.done();
});

// Pjax发送请求失败时关闭进度条
document.addEventListener("pjax:error", function () {
  NProgress.done();
});

后记

参考链接

标签: 前端优化, 前端

添加新评论