全部 / 技术交流 · 2023年4月1日 1

WordPress主题 PJAX 无刷新以及渲染问题的修复

本文最后更新于 600 天前,其中的信息可能已经有所发展或改变。

近期为自己的wp主题配置了PJAX~此前使用的是Turbolinks插件,因为插件属于不可控元素,产生过多BUG后还是决定手动配置更为先进的PJAX(其实BUG更多了,但至少可以逐一排查...)

原理

PJAX原理

GitHub - defunkt/jquery-pjax: pushState + ajax = pjax
pushState + ajax = pjax. Contribute to defunkt/jquery-pjax development by creating an account on GitHub.
github.com

PJAX(Pushstate + Ajax)是一种用于加快网页加载速度的技术。它结合了HTML5的pushState API和Ajax技术,使得在不刷新整个页面的情况下,可以实现局部页面内容的更新。PJAX的主要原理可以分为以下几个步骤:

  1. 用户点击链接:当用户点击一个链接时,PJAX会拦截该链接的点击事件。
  2. 阻止默认行为:PJAX会阻止浏览器执行默认的链接跳转行为,避免整个页面的刷新。
  3. 发送Ajax请求:接着,PJAX会通过Ajax发送一个请求到服务器,请求目标链接的内容。这个请求通常会包含一个特殊的请求头,以便服务器能够识别这是一个PJAX请求,从而仅返回局部更新所需的内容,而非整个页面的HTML代码。
  4. 更新URL和页面内容:当服务器返回局部内容后,PJAX会利用HTML5的pushState API来更新浏览器地址栏中的URL,然后用返回的内容替换页面中指定的容器元素。这样一来,用户就会感觉像是进行了一次正常的页面跳转,但实际上只是局部内容发生了更新。
  5. 更新浏览器历史记录:通过pushState API更新URL之后,浏览器的历史记录也会相应地更新。这样,用户在点击浏览器的前进和后退按钮时,可以按照预期的方式导航。

PJAX的优势在于减少了不必要的页面刷新,提高了用户体验。同时,由于只需要加载局部内容,它还有助于降低服务器负载。然而,PJAX也有一些局限性,例如对于不支持HTML5 pushState API的浏览器,PJAX可能无法正常工作。

引入PJAX

引入JS文件

前往主题的function.php,添加如下代码入队:

function add_pjax() {
    wp_register_script('pjax', 'https://cdn.bootcss.com/jquery.pjax/2.0.1/jquery.pjax.min.js', array('jquery'), '2.0.1', true);
    wp_enqueue_script('pjax');
}
add_action('wp_enqueue_scripts', 'add_pjax');
PHP

包裹PJAX容器

本段部分参考至https://alpha.skywt.cn/post/typecho-pjax-and-some-problems

F12检查自己博客的html结构,找到需要PJAX动态刷新的类,并将它包裹在PJAX容器中:

一般的网页结构:

<html>
    <head>
        <!-- ... -->
    </head>
    <body>
        <header><!-- 页眉,标题什么的 --></header>
        <nav><!-- 导航栏什么的 --></nav>
        <main id="pjax-container">
            <!-- 网站主体内容,文章列表 / 文章内容 / 评论什么的 -->
        </main>
        <script>
            // 下面要加上的代码
        </script>
        <footer><!-- 页脚什么的 --></footer>
    </body>
</html>
HTML

只需前往header.php以及footer.php中找到相应部分即可,在PJAX容器后方添加初始化代码(这里是Wordpress版本):

<script>
	jQuery(document).ready(function($) {
    $(document).pjax('a[href^="<?php echo home_url(); ?>"]:not(a[target="_blank"])', {
        container: '#pjax-container',
        fragment: '#pjax-container'
    });

    $(document).on('pjax:send', function() {
        $('#pjax-container').fadeTo(700,0.0); //这里添加了一个淡入动画
        // alert('开始加载');
    // 开始加载时要运行的代码(如显示加载动画)
    });

    $(document).on('pjax:complete', function() {
        $('#pjax-container').fadeTo(700,1);
        // 完成加载时要运行的代码
    });
});
</script>
HTML

保存并清除缓存后,刷新后打开开发者工具,网络选项搜索PJAX,看见?_pjax=#pjax-container意味着PJAX配置成功。

加载指示器

引入PJAX时,用户点击超链后浏览器将不作回应,因此需要使用加载指示器显示页面正在加载中,HTML中加入

<div id="loading-spinner" class="loader" style="display:none;"></div>
HTML

对应CSS(这里使用CSS矢量动画作为加载指示)

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

#loading-spinner {
  position: fixed;
  top: 20px;
  right: 20px;
  width: 24px;
  height: 24px;
  border: 2px solid #ccc;
  border-top: 2px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  z-index: 9999;
}
CSS

PJAX发送时,加载动画出现,完成时隐藏,这是我的完整JS

jQuery(document).ready(function ($) {
    $(document).pjax('a[href^="<?php echo home_url(); ?>"]:not(a[target="_blank"])', {
        container: '#pjax-container',
        fragment: '#pjax-container'
    });

    $(document).on('pjax:send', function () {
        // 显示加载指示器
        $('#loading-spinner').show();

        window.scrollTo({ top: 0, behavior: 'smooth' });
        $('#pjax-container').fadeTo(400, 0.0);
        $('#toc').empty();
    });

    $(document).on('pjax:complete', function () {
        // 隐藏加载指示器
        $('#loading-spinner').hide();

        // 初始化左侧小组件
        Toc.init({
            $nav: $('#toc')
        });
        $('body').scrollspy({
            target: '#toc'
        });
        
        trackPjaxPageView();
        
        $('#pjax-container').fadeTo(400, 1);
    });
});
JavaScript

遇到的一些问题

PJAX意味着随之而来的无数BUG...

Matomo追踪代码

如果启用了Matomo或其他的一些追踪代码(它们一般放在Header内),意味着用户通过PJAX进入的界面无法被记入统计数据,因此必须在PJAX完成后重新载入这段JS,首先定义一个新函数

function trackPjaxPageView() {
    if (typeof _paq !== 'undefined') {
        _paq.push(['setReferrerUrl', document.referrer]);
        _paq.push(['setDocumentTitle', document.title]);
        _paq.push(['setCustomUrl', window.location.href]);
        _paq.push(['trackPageView']);
    }
}
JavaScript

PJAX完成后调用它:

$(document).on('pjax:complete', function() {
		trackPjaxPageView();
        $('#pjax-container').fadeTo(700,1);
    });

var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
    var u="//analy.hiripple.com/";//这是我的matomo地址
    _paq.push(['setTrackerUrl', u+'matomo.php']);
    _paq.push(['setSiteId', '1']);
    var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
    g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
JavaScript

左侧目录刷新

将PJAX容器包裹在正文,意味着左侧小工具栏所有元素都不会刷新(例如这个使用了bootstrap-toc.js的小目录)

$(document).on('pjax:complete', function() {
        // 初始化左侧小组件,查询官方的说明文档
        Toc.init({
            $nav: $('#toc'),
            $scope: $(document.body)
        });
        $('body').scrollspy({
            target: '#toc'
        });
        $('#pjax-container').fadeTo(700,1);
    });
JavaScript

即使重新初始化,也存在前一界面目录未清除的问题,在发送PJAX时清除目录:

$(document).on('pjax:send', function() {
        $('#pjax-container').fadeTo(700,0.0);
		$('#toc').empty(); //清除目录
    });
JavaScript

Lazyload/lightbox

PJAX可能会导致Lazyload无法加载的情况:

function pjax_reload() {
  var imgs = document.querySelectorAll("#main img.lazyload");
  lazyload(imgs);
}
jQuery(document).ready(function($) {

    // 初始化fancybox
    jQuery('[data-fancybox="gallery"]').fancybox();

    // 初始化lazyload
    pjax_reload();

    $(document).pjax('a[href^="<?php echo home_url(); ?>"]:not(a[target="_blank"])', {
        container: '#pjax-container',
        fragment: '#pjax-container'
    });

    $(document).on('pjax:send', function() {
        $('#pjax-container').fadeTo(700,0.0);
    });

    $(document).on('pjax:complete', function() {
        jQuery('[data-fancybox="gallery"]').fancybox();

        // 重新初始化lazyload
        pjax_reload();

        $('#pjax-container').fadeTo(700,1);
    });
});
JavaScript

或者直接使用图片loading='lazy'属性,这是HTML5中的原生懒加载。原生懒加载不需要额外的处理,浏览器会自动处理懒加载。

点击事件

这里需要切换夜间模式

function initDarkModeToggle() {
  const toggleBtnMoon = document.getElementById("moon-icon");
  const toggleBtnSun = document.getElementById("sun-icon");

  function applyDarkMode() {
    document.body.classList.add("dark-mode");
    localStorage.setItem("theme", "dark-mode");
    toggleBtnMoon.style.display = "none";
    toggleBtnSun.style.display = "block";
  }

  function removeDarkMode() {
    document.body.classList.remove("dark-mode");
    localStorage.setItem("theme", "light-mode");
    toggleBtnMoon.style.display = "block";
    toggleBtnSun.style.display = "none";
  }

  if (localStorage.getItem("theme") === "dark-mode") {
    applyDarkMode();
  } else {
    removeDarkMode();
  }

  toggleBtnMoon.addEventListener("click", applyDarkMode);
  toggleBtnSun.addEventListener("click", removeDarkMode);
}
$(document).on('pjax:complete', function() {
  initDarkModeToggle();
});
JavaScript