PJAX(pushState + ajax)是一种页面加载方式,点击链接时,通过 AJAX 无刷新的从服务器请求 HTML 内容,然后用请求到的内容来更新页面,可以实现类似于单页应用的使用体验,而且不会影响搜索引擎抓取页面。

差不多在两年前就有用户提议给主题加入 PJAX,但是 Typecho 程序和 PJAX 兼容的不太好,使用 PJAX 时,Typecho 评论的 反垃圾保护 是无法使用的,所以我一直没有加 PJAX。

考虑到 PJAX 确实可以提升浏览体验,而且也有用户需要这个功能,现在我给我的两个主题 MWordStarFacile 都加入了 PJAX,用户可以根据需求选择开启或关闭。

下面就是 Typecho 主题使用 PJAX 的方式和一些需要注意的地方。

兼容性

Typecho 程序在提交评论时,除了提交评论用户能看到的评论表单内容外,还会提交一个隐藏的表单来校验评论是否是从本网站提交的,也会检查评论的提交页面 URL 是否是文章 URL。如果要使用 PJAX 就需要在 Typecho 后台的评论设置关闭下面两项设置:

  • 检查评论来源页 URL 是否与文章链接一致
  • 开启反垃圾保护

关闭后可以正常使用 PJAX 无刷新提交评论,但是因为不会校验评论提交,所以无法防止其它程序提交垃圾评论。

下载和引入

这里使用的 PJAX 是一个 jQuery 插件,需要依赖 jQuery 才能使用。

你可以直接使用 script 引入 https://cdn.jsdelivr.net/npm/jquery-pjax@2.0.1/jquery.pjax.min.js,国内访问的速度可能会慢一些,你也可以直接下载 JS 文件,通过 script 引入下载的 JS 文件。

这是一个 jQuery 插件,你的引入顺序应该是:

  1. jquery
  2. jquery.pjax
  3. 你自己的 JS 文件

我下面的 JS 代码也会使用 jQuery 的方式来操作 DOM 之类的。

PJAX 链接添加 class

PJAX 使用 jQuery 选择器来给链接添加 PJAX 请求,如果你直接使用 $('a') 来选择链接的话,会给所有链接都添加 PJAX。我个人的选择是只给包含我网站域名的链接添加 PJAX,而且链接不能包含 target="_blank" 属性。

下面给包含我网站域名的链接添加一个 pjax-linkclass

// 获取当前域名
const currentDomain = window.location.hostname;

$('a').each((index, element) => {
  const href = $(element).attr('href');
  const target = $(element).attr('target');

  // 检查链接是否包含当前域名,且不含有 target="_blank"
  if (href && href.includes(currentDomain) && !target) {
    $(element).addClass('pjax-link');
  }
});

只有包含 pjax-link class 的链接才会添加 PJAX。

PJAX 容器元素

PJAX 需要一个容器元素,AJAX 请求完成后只会更新容器元素内的内容,每个页面都需要有相同的容器元素,如果没有容器元素页面就会刷新。

如果你想省事的话,可以直接在 body 内放一个 idappdiv,把除了 script 的所有内容,包括顶部导航栏、内容区域、和底部内容都放到这个 div 里,请求完成后会直接更新这个 div 内的所有内容。你也不需要手动更改导航栏链接的选中状态。

上面的方式比较简单,但是也会增加一些不必要的 DOM 处理和替换。如果你能手动处理导航栏样式的话,可以只给内容部分添加容器元素,AJAX 请求完成后也只会更新内容部分。

链接绑定 PJAX

上面已经给需要 PJAX 请求的链接加入了一个 pjax-linkclass,下面就给这些链接绑定 PJAX:

$(document).pjax('.pjax-link', '#main', {
  fragment: '#main',
  timeout: 20000
});

上面的 $(document).pjax 的第一个参数就是绑定 PJAX 的链接,第二个参数是容器元素,我的容器元素是一个 idmaindiv,第三个参数是选项。

下面是选项说明:

  • fragment:新页面的容器元素
  • timeout:超时(毫秒),如果超时页面就会刷新

给链接绑定 PJAX 后,点击链接就可以无刷新跳转了。

表单绑定 PJAX

Typecho 的评论和搜索是通过 form 表单提交的,为了方便区分,你可以给这两个 form 表单添加一个 id,比如 comment-formsearch-form

下面给表单绑定 PJAX:

$(document).on('submit', '#comment-form', ev => {
  $.pjax.submit(ev, '#main', {
    fragment: '#main',
    replace: true,
    push: false,
    timeout: 20000
  });
});

上面的 $(document).on 是监听评论表单的提交事件,我监听的是 id 为 comment-form 的评论表单。$.pjax.submit 是使用 PJAX 提交表单,第一个参数是表单提交事件的 event 对象,第二个参数是容器元素,第三个参数是选项。

下面是用到的一些选项说明:

  • replace:只替换 URL,不添加历史记录
  • push:添加浏览器历史记录

fragmenttimeout 和上面的链接选项是一样的。

PJAX 事件

PJAX 提供了一些事件,这些事件会在 PJAX 请求的不同阶段被触发,下面是 PJAX 提供的事件:

$(document).on('pjax:start', () => {
  // PJAX 即将开始请求
});

$(document).on('pjax:send', () => {
  // PJAX 开始请求
});

$(document).on('pjax:complete', () => {
  // PJAX 请求完成
});

$(document).on('pjax:end', ev => {
  // PJAX 页面更新完成
});

其中的 pjax:end 事件是用的比较多的,这个事件会在页面更新完成后触发,一些页面初始化的 JS,比如代码高亮之类的就会放到这个事件里。

pjax:end 事件的 event 对象的 currentTarget.URL 可以获取点击链接的 URL,如果你需要设置导航栏链接选中状态的话,可以通过这个 URL 来判断设置。

评论回复问题

如果你从其它页面通过 PJAX 进入文章页,也就是你一开始打开的不是文章页,点击回复评论的时候,评论表单可能无法插入到回复区域。

Typecho 的文章页和独立页面的 head 区域会输出一段 JS 代码,这段 JS 代码包含了页面显示的所有评论的 coid,当你点击回复评论的时候,就会通过 coid 来把评论表单插入到回复位置。

PJAX 的容器元素一般都是 body 里的元素,更新页面的时候不会替换 head 里的元素,所以回复相关的 JS 代码也就无法被插入到页面。

下面的代码需要放到 PJAX 的容器元素里,你可以放到 comment.php 或其它评论相关的 PHP 文件中,需要确保 JS 代码能在文章页和独立页面的容器元素中输出:

<script type="text/javascript">
  (function() {
    window.TypechoComment = {
      dom: function(id) {
        return document.getElementById(id);
      },
      create: function(tag, attr) {
        var el = document.createElement(tag);
        for (var key in attr) {
          el.setAttribute(key, attr[key]);
        }
        return el;
      },
      reply: function(cid, coid) {
        var comment = this.dom(cid),
            parent = comment.parentNode,
            response = this.dom('<?php echo $this->respondId; ?>'),
            input = this.dom('comment-parent'),
            form = 'form' == response.tagName ? response : response.getElementsByTagName('form')[0],
            textarea = response.getElementsByTagName('textarea')[0];
        if (null == input) {
          input = this.create('input', {
            'type': 'hidden',
            'name': 'parent',
            'id': 'comment-parent'
          });
          form.appendChild(input);
        }
        input.setAttribute('value', coid);
        if (null == this.dom('comment-form-place-holder')) {
          var holder = this.create('div', {
            'id': 'comment-form-place-holder'
          });
          response.parentNode.insertBefore(holder, response);
        }
        comment.appendChild(response);
        this.dom('cancel-comment-reply-link').style.display = '';
        if (null != textarea && 'text' == textarea.name) {
          textarea.focus();
        }
        return false;
      },
      cancelReply: function() {
        var response = this.dom('<?php echo $this->respondId; ?>'),
            holder = this.dom('comment-form-place-holder'),
            input = this.dom('comment-parent');
        if (null != input) {
          input.parentNode.removeChild(input);
        }
        if (null == holder) {
          return true;
        }
        this.dom('cancel-comment-reply-link').style.display = 'none';
        holder.parentNode.insertBefore(response, holder);
        return false;
      }
    };
  })();
</script>

PJAX 更新页面的时候,也会把上面的代码插入到页面中。

类似文章: