Featured image of post 【本文为 AI 生成】Hugo + Pjax 实现无刷新博客体验:从音乐播放中断谈起

【本文为 AI 生成】Hugo + Pjax 实现无刷新博客体验:从音乐播放中断谈起

记录如何通过引入 Pjax 将 Hugo 静态博客改造为 SPA(单页应用),解决页面切换导致音乐播放中断、脚本失效等一系列技术难题。

前言

在搭建个人博客的过程中,我一直有一个执念:希望网页底部的音乐播放器能够像网易云音乐那样,在页面切换时永不中断

传统的静态博客(如 Hugo 生成的站点)每一次点击链接,浏览器都会重新加载整个页面 (Full Page Reload)。这意味着:

  1. DOM 树被销毁重建。
  2. 所有 JavaScript 状态丢失。
  3. 音频/视频标签被重置 —— 这就是为什么音乐会停。

为了解决这个问题,我们需要引入 SPA (Single Page Application) 的概念,或者更轻量级的方案 —— Pjax (PushState + Ajax)

核心技术方案:Pjax

Pjax 的工作原理非常直观:

  1. 拦截 <a> 标签的点击事件。
  2. 使用 Ajax 请求新页面的 HTML 内容。
  3. 解析新 HTML,只提取我们需要更新的部分(例如主要内容区 .main-container)。
  4. 使用 history.pushState 修改浏览器的 URL地址栏,使其看起来像正常跳转。
  5. 替换 DOM 中的内容区。

通过这种方式,页脚 (Footer)侧边栏 (Sidebar) 可以保持不变,驻留在其中的音乐播放器自然也就不会中断了。

1. 引入 Pjax

首先在 <head> 中引入 Pjax 库(推荐使用 pjax 库而非老旧的 jquery-pjax):

1
<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script>

2. 定制化配置 (The Tricky Part)

这是最关键的一步。为了保证 Stack 主题的正常渲染,如果你直接替换整个 body,播放器还是会挂掉。我们需要精准打击

我在 layouts/partials/head/custom.html 中进行了如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var pjax = new Pjax({
    selectors: [
        "title",
        ".main-container",  // 只替换内容区!
        "body"              // 这里的处理很有讲究,见下文
    ],
    switches: {
        "body": function(oldEl, newEl, options) {
            // 我们只更新 body 的 class (用于切换暗色模式或页面特定样式)
            // 绝不替换 body 的 innerHTML,否则页脚脚本会被杀掉
            oldEl.className = newEl.className;
        },
        ".main-container": Pjax.switches.innerHTML, // 标准替换
        "title": Pjax.switches.outerHTML
    }
});

关键点:不仅要通过 CSS 选择器指定更新区域,还要自定义 switch 函数,确保 body 标签只更新属性而不重置内容。

踩坑与填坑

实现 Pjax 只是第一步,真正的挑战在于副作用

坑一:脚本不执行 (Mastodon 动态消失)

现象:跳转到 Timeline 页面,Mastodon 动态加载不出来。 原因:通过 innerHTML 插入的 HTML 片段中如果包含 <script> 标签,浏览器出于安全和规范考虑,通常不会执行它们

解决方案: 我们将初始化代码封装为全局函数,并在 Pjax 完成事件 (pjax:complete) 中手动调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Check & Init Mastodon Logic
window.initMastodon = function() {
    if (!document.getElementById('mt-container')) return;
    // ... 初始化代码 ...
};

// 监听 Pjax 完成
document.addEventListener('pjax:complete', function () {
    window.initMastodon(); 
    // 其他需要重载的脚本,如 Google Analytics
    if (typeof gtag === 'function') {
        gtag('config', 'MEASUREMENT_ID', {'page_path': location.pathname});
    }
});

坑二:播放器状态丢失

现象:虽然使用了 Pjax,但用户有时会习惯性按 F5 刷新,或者 Pjax 请求超时回退到普通跳转,这时候音乐还是会断,且进度归零。

解决方案:状态持久化 (State Persistence)。

利用 localStorage 在播放器每秒更新时记录状态:

1
2
3
4
5
6
7
setInterval(() => {
    if (!ap.audio.paused) { 
        localStorage.setItem('aplayer_time', ap.audio.currentTime);
        localStorage.setItem('aplayer_index', ap.list.index);
        localStorage.setItem('aplayer_paused', 'false');
    }
}, 1000);

在页面加载时(无论是 Pjax 还是普通加载),尝试恢复状态:

1
2
3
4
5
const savedTime = localStorage.getItem('aplayer_time');
if (savedTime) {
    ap.seek(parseFloat(savedTime));
    if (savedPaused === 'false') ap.play();
}

这里还有一个细节:audio 元素必须在元数据加载后才能 seek,所以需要监听 loadedmetadatacanplay 事件。

总结

通过引入 Pjax 并配合精细的生命周期管理,我们成功在静态博客上实现了类似 SPA 的流畅体验:

  1. 音乐不间断:Footer 区域脱离了页面刷新的生命周期。
  2. 加载极速:只请求部分 HTML,带宽消耗更低。
  3. 体验降级:即使 Pjax 失败,完善的状态恢复机制也能保证用户体验不割裂。

折腾博客的乐趣往往不在于写文章本身,而在于通过解决这些具体的技术问题,窥探现代 Web 开发的冰山一角。

潇洒人间一键仙
使用 Hugo 构建
主题 StackJimmy 设计