一切的开端

ciri老师装修博客,和我分享了她在看的教程。

我看第一遍:我超这什么大补丁工程天书,这也太难了吧改动这么多我绝对不干。
看第二遍:虽然还是看不太懂,但是它实现的功能好棒啊而且好心动,歌单持续播放好酷啊!
看第三遍:…………………………好像可行。

于是吃完饭就坐进了装修的牢房里。

Todo:引入pjax实现音乐播放不中断

Pjax是一个独立的JavaScript模块,它使用AJAX和pushState来提供快速的浏览体验。使用户感觉像在浏览一个应用程序,不再有完整页面的重新加载,也不再有多次 HTTP 请求,每次切换页面时,pjax会对网页内容进行局部加载,这样就不会出现网页全局刷新导致的aplayer音乐中断。

类比的话可以拿ff14举例,没有pjax的是普通副本,每次想换职业都得等到出副本,切换,然后才能用新职业再进下一个副本。有pjax的是多变迷宫副本,可以在副本里无缝切换职业,非常快乐(?

参考文献

【Hugo】Aplayer + PJAX 引入音乐播放放器并实现音乐不中断功能↗

Hugo 添加 Pjax 功能支持↗

MoOx/pjax↗(使用的pjax版本)

准备工作:音乐播放进度保留

在这一份■■Loading:《hugo装修日志05》■■↗装修报告里提到的渲染的全局歌单music.htmlhtml文件下添加这一段代码。实现刷新网页后音乐可以中断处继续播放的效果(有一定的延迟

<script>
    ...
    /**
     * 页面销毁前监听
     */
    window.onbeforeunload = () => {
        // 将播放信息用对象封装,并存入到localStorage中
        const playInfo = {
            index: ap.list.index,
            currentTime: ap.audio.currentTime,
            paused: ap.paused
        };
        localStorage.setItem("playInfo", JSON.stringify(playInfo));
    };

    /**
     * 页面加载后监听
     */
    window.onload = () => {
        // 从localStorage取出播放信息
        const playInfo = JSON.parse(localStorage.getItem("playInfo"));
        if (!playInfo) {
            return;
        }
        // 切换歌曲
        ap.list.switch(playInfo.index);
        // 等待500ms再执行下一步(切换歌曲需要点时间,不能立马调歌曲进度条)
        setTimeout(() => {
            // 调整时长
            ap.seek(playInfo.currentTime);
            // 是否播放
            if (!playInfo.paused) {
                ap.play()
            }
        }, 500);
    };
</script>

引入pjax

这里只记录了我的个人引入方法。

1.确认需要博客网页需要重刷新的部分,检查/layouts/_default/baseof.html文件。

    <main>
        <div class="container-lg clearfix">
            <!-- list -->
            <div class="col-12 col-md-9 float-left content">
                {{ block "main" . }}{{ end }}
            </div>
            {{ partial "sidebar.html" . }}
        </div>
        {{ partial "components.html" . }}
    </main>

可以看到<main>标签了包裹了网站的主体部分。此外标题title也会改变,所以只用让pjax监听这两个模块。

2.在/layouts/patials目录里新建pjax.html,复制以下代码。

<script>
    var pjax = new Pjax({
        selectors: [
            "main","title" // 添加需要监听的选择器
        ]
    });
</script>

3.最后在baseof.html里渲染这个代码,虽然直接写进baseof里也可以达到相同的效果,但为了代码主题美观和方便修改,所以单独把pjax部分提了出来。

{{ partial "pjax.html" . }}

推送至github,等待服务器deploy完后刷新网站,如果切换页面时音乐能持续播放,说明引入成功。

当然这只是一切的开始,因为随即我们就会发现pjax把博客很多样式和功能都干碎了,只能一步一步着手重新修复它们。

主题切换和返回顶部按钮修复

1.在fuji主题文件的/assets/js/fuji.js目录文件里找到关于这两个按钮的js代码。

//主题切换
document.querySelector('.btn .btn-toggle-mode').addEventListener('click', () => {
  let nowTheme = getNowTheme();
  let domTheme = document.body.getAttribute('data-theme');
  const needAuto = document.body.getAttribute('data-theme-auto') === 'true';
  let systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';

  if (domTheme === 'auto') {
    // if now in auto mode, switch to user mode
    document.body.setAttribute('data-theme', nowTheme === 'light' ? 'dark' : 'light');
    localStorage.setItem('fuji_data-theme', nowTheme === 'light' ? 'dark' : 'light');
  } else if (domTheme === 'light') {
    const tar = systemTheme === 'dark' ? (needAuto ? 'auto' : 'dark') : 'dark';
    document.body.setAttribute('data-theme', tar);
    localStorage.setItem('fuji_data-theme', tar);
  } else {
    const tar = systemTheme === 'light' ? (needAuto ? 'auto' : 'light') : 'light';
    document.body.setAttribute('data-theme', tar);
    localStorage.setItem('fuji_data-theme', tar);
  }

//滚动顶部
document.querySelector('.btn .btn-scroll-top').addEventListener('click', () => {
  document.documentElement.scrollTop = 0;
});  

2.询问全世界最完美的修博客搭档ChatGPT,让它将这些代码封装入一个函数中,以便在页面初次加载和 Pjax 更新完成后都能重新绑定事件监听器。

// 封装主题切换功能为一个函数
function initThemeToggle() {
  const toggleButton = document.querySelector('.btn .btn-toggle-mode');
  if (!toggleButton) return; // 确保按钮存在

  // 绑定主题切换按钮的点击事件
  toggleButton.addEventListener('click', () => {
    let nowTheme = getNowTheme();
    let domTheme = document.body.getAttribute('data-theme');
    const needAuto = document.body.getAttribute('data-theme-auto') === 'true';
    let systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';

    if (domTheme === 'auto') {
      document.body.setAttribute('data-theme', nowTheme === 'light' ? 'dark' : 'light');
      localStorage.setItem('fuji_data-theme', nowTheme === 'light' ? 'dark' : 'light');
    } else if (domTheme === 'light') {
      const tar = systemTheme === 'dark' ? (needAuto ? 'auto' : 'dark') : 'dark';
      document.body.setAttribute('data-theme', tar);
      localStorage.setItem('fuji_data-theme', tar);
    } else {
      const tar = systemTheme === 'light' ? (needAuto ? 'auto' : 'light') : 'light';
      document.body.setAttribute('data-theme', tar);
      localStorage.setItem('fuji_data-theme', tar);
    }
  });
}

// 在页面初次加载时调用
initThemeToggle();

// 监听 Pjax 完成事件并调用封装的函数
document.addEventListener("pjax:complete", function () {
  initThemeToggle();
});

// 封装滚动到顶部按钮的功能
function initScrollTopButton() {
  const scrollTopButton = document.querySelector('.btn .btn-scroll-top');
  if (!scrollTopButton) return; // 确保按钮存在

  // 绑定滚动到顶部按钮的点击事件
  scrollTopButton.addEventListener('click', () => {
    document.documentElement.scrollTop = 0; // 滚动到页面顶部
  });
}

// 在页面初次加载时调用
initScrollTopButton();

// 监听 Pjax 完成事件并重新调用封装的函数
document.addEventListener("pjax:complete", function () {
  initScrollTopButton();
});

这样两个按钮就都修好了!手机端的菜单唤起按钮修法同上。

// 封装菜单显示/隐藏功能
function initMenuToggle() {
  const openMenu = document.getElementById('btn-menu');
  if (!openMenu) return; // 确保菜单按钮存在

  // 绑定菜单按钮的点击事件
  openMenu.addEventListener('click', () => {
    const menu = document.querySelector('.sidebar-mobile');
    if (!menu) return; // 确保菜单存在

    // 切换菜单的显示状态
    if (menu.style.display === 'none' || !menu.style.display) {
      menu.style.display = 'flex'; // 显示菜单
    } else {
      menu.style.display = 'none'; // 隐藏菜单
    }
  });
}

// 页面初次加载时调用
initMenuToggle();

// 监听 Pjax 完成事件,并重新调用封装的函数
document.addEventListener('pjax:complete', function () {
  initMenuToggle();
});

waline评论区显示修复

官方文档↗Q&A里给出了disqus评论区的修复示例,虽然没有完全看懂,但就这么去问ChatGPT了!

1.封装waline渲染脚本,我的waline是自己动手配置的,详见■■Loading:《hugo装修日志02》■■↗

<hr />
     <head>
        <!-- ... -->
        <link
          rel="stylesheet"
          href="/css/waline.css"
        />
        <!-- ... -->
      </head>
      <body>
        <!-- ... -->
        <div id="waline"></div>
        <script type="module">
          import { init } from 'https://unpkg.com/@waline/client@v2/dist/waline.mjs';
      
          function setupWaline() {
  init({
    el: '#waline',
    serverURL: '填写你的',
    dark: 'body[data-theme="dark"]',
    requiredMeta: ['nick', 'mail'],
    search: false,
    locale: {
      placeholder: '占位符'
    },
    avatar: 'retro',
    emoji: [
      '填写你的。'
    ],
  });
}

// 绑定 Waline 初始化到 Pjax 完成事件
document.addEventListener('pjax:complete', setupWaline);

// 立即初始化 Waline
document.addEventListener('DOMContentLoaded', setupWaline);
        </script>
      </body>      

2.根据官方文档的and be sure to have at least an empty wrapper on each page (to avoid differences of DOM between pages),我们需要在baseof.html,也就是网站主体里放置一个空的评论区容器,这些改动的代码我也全部写到了pjax.html里。

<script>
function setupWaline() {
        if (document.querySelector('#waline')) {
            import('https://unpkg.com/@waline/client@v2/dist/waline.mjs').then(({ init }) => {
                init({
                    el: '#waline',
                    serverURL: '',
                    dark: 'body[data-theme="dark"]',
                    requiredMeta: ['nick', 'mail'],
                    search: false,
                    locale: {
                        placeholder: ''
                    },
                    avatar: 'retro',
                    emoji: [
                    ],
                });
            });
        }
    }



    // 在页面初次加载时初始化 Waline
    document.addEventListener('DOMContentLoaded', setupWaline);

    // 监听 PJAX 完成事件并初始化 Waline
    document.addEventListener('pjax:complete', setupWaline);
</script>

推送github,发现评论区还是需要二次刷新才能看见。遂继续研究fuji主题里的js,发现有一段代码用于根据当前页面的评论区的状态和类型加载评论内容。

 // switch comment area theme
  // if this page has comment area
  let commentArea = document.querySelector('.post-comment');
  if (commentArea) {
    // if comment area loaded
    if (document.querySelector('span.post-comment-notloaded').getAttribute('style')) {
      if (commentArea.getAttribute('data-comment') === 'utterances') {
        updateUtterancesTheme(document.querySelector('.post-comment iframe'));
      }
      if (commentArea.getAttribute('data-comment') === 'disqus') {
        DISQUS.reset({
          reload: true,
        });
      }
    }
  };

3.让ChatGPT封装了这个代码,以防万一也做了个空容器。

fuji.js

// 封装评论区域主题切换功能为一个函数
function updateCommentAreaTheme() {
  // 查找评论区域
  let commentArea = document.querySelector('.post-comment');
  
  if (commentArea) {
    // 如果评论区域已加载
    let notLoadedSpan = document.querySelector('span.post-comment-notloaded');
    if (notLoadedSpan && notLoadedSpan.getAttribute('style')) {
      // 根据评论系统更新主题
      if (commentArea.getAttribute('data-comment') === 'utterances') {
        updateUtterancesTheme(document.querySelector('.post-comment iframe'));
      }
      if (commentArea.getAttribute('data-comment') === 'disqus') {
        DISQUS.reset({
          reload: true,
        });
      }
    }
  }
}

// 在页面初次加载时调用
updateCommentAreaTheme();

// 监听 Pjax 完成事件并调用封装的函数
document.addEventListener("pjax:complete", function () {
  updateCommentAreaTheme();
});

pjax.html

<script>
// 封装评论区域主题切换功能为一个函数
function updateCommentAreaTheme() {
  // 查找评论区域
  let commentArea = document.querySelector('.post-comment');
  
  if (commentArea) {
    // 如果评论区域已加载
    let notLoadedSpan = document.querySelector('span.post-comment-notloaded');
    if (notLoadedSpan && notLoadedSpan.getAttribute('style')) {
      // 根据评论系统更新主题
      if (commentArea.getAttribute('data-comment') === 'utterances') {
        updateUtterancesTheme(document.querySelector('.post-comment iframe'));
      }
      if (commentArea.getAttribute('data-comment') === 'disqus') {
        DISQUS.reset({
          reload: true,
        });
      }
    }
  }
}

// 在页面初次加载时调用
updateCommentAreaTheme();

// 监听 Pjax 完成事件并调用封装的函数
document.addEventListener("pjax:complete", function () {
  updateCommentAreaTheme();
});
</script>

再次推送刷新,这次就修好了!我最爱的评论区,复活!

代码块样式修复

无意点进一篇原来的装修日志,发现代码块的样式直接被pjax格式化了,只得继续闷头大修。

1.找到fuji主题里与代码颜色相关的外部js文件,最后发现在layouts/patials/scripts-end.html里:

<script defer src="https://gcore.jsdelivr.net/npm/[email protected]/components/prism-core.min.js" integrity="sha512-LCKPTo0gtJ74zCNMbWw04ltmujpzSR4oW+fgN+Y1YclhM5ZrHCZQAJE4quEodcI/G122sRhSGU2BsSRUZ2Gu3w==" crossorigin="anonymous"></script>
<script defer src="https://gcore.jsdelivr.net/npm/[email protected]/plugins/autoloader/prism-autoloader.min.js" integrity="sha512-GP4x8UWxWyh4BMbyJGOGneiTbkrWEF5izsVJByzVLodP8CuJH/n936+yQDMJJrOPUHLgyPbLiGw2rXmdvGdXHA==" crossorigin="anonymous"></script>

2.让ChatGPT写一个pjax初始化代码,直接复制到该文件下方。

<!-- PJAX 事件处理 -->
<script>
  // 函数用于重新初始化 PrismJS 语法高亮
  function initPrism() {
    if (typeof Prism !== 'undefined') {
      Prism.highlightAll();
    }
  }

  // PJAX 事件处理
  $(document).on('pjax:complete', function() {
    initPrism(); // 重新初始化 PrismJS
  });

  // 在页面加载时初始化 PrismJS
  $(document).ready(function() {
    initPrism();
  });
</script>

推送发现代码块格式修好了,但是后期加上去的copy按钮(详见↗)依旧没有恢复。

3.封装代码copy按钮的js代码。

// Function to initialize copy buttons for code blocks
function initCopyButtons() {
  var codeblocks = document.getElementsByTagName("pre");
  // Loop through each code block and add copy button
  for (var i = 0; i < codeblocks.length; i++) {
      // Show copy code button
      var currentCode = codeblocks[i];
      currentCode.style = "position: relative;";
      var copy = document.createElement("div");
      copy.style = "position: absolute;right: 4px;top: 4px;background-color: white;padding: 2px 8px;margin: 8px;border-radius: 4px;cursor: pointer;z-index: 9999;box-shadow: 0 2px 4px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.05);";
      copy.innerHTML = "Copy";
      currentCode.appendChild(copy);
      // Hide all copy buttons initially
      copy.style.visibility = "hidden";
  }

  // Add event listeners to code blocks
  for (var i = 0; i < codeblocks.length; i++) {
      !function(i) {
          // Show button on mouseover
          codeblocks[i].onmouseover = function() {
              codeblocks[i].childNodes[1].style.visibility = "visible";
          };

          // Function to handle copy action
          function copyArticle(event) {
              const range = document.createRange();
              // Range is the code block, excluding the created div
              range.selectNode(codeblocks[i].childNodes[0]);
              const selection = window.getSelection();
              if (selection.rangeCount > 0) selection.removeAllRanges();
              selection.addRange(range);
              document.execCommand('copy');
              codeblocks[i].childNodes[1].innerHTML = "Copied!";
              setTimeout(function() {
                  codeblocks[i].childNodes[1].innerHTML = "ReCopy";
              }, 1000);
              // Clear selection
              if (selection.rangeCount > 0) selection.removeAllRanges();
          }
          codeblocks[i].childNodes[1].addEventListener('click', copyArticle, false);

      }(i);

      !function(i) {
          // Hide button on mouseout
          codeblocks[i].onmouseout = function() {
              codeblocks[i].childNodes[1].style.visibility = "hidden";
          };
      }(i);
  }
}

// Initialize copy buttons on page load
document.addEventListener('DOMContentLoaded', initCopyButtons);

// Reinitialize copy buttons after PJAX completes
document.addEventListener('pjax:complete', initCopyButtons);

推送之后,发现复制按钮还是死的(悲)。之前我是把这个js文件放在/static/js目录下,然后在/layouts/patials/footer.html里引用。尝试直接把整段js代码复制到fuji.js里面,成功了,大概是因为footer.html里的内容不在pjax的监听范围内,所以需要放到main标签引用的模块里。

搜索功能修复

原本的搜索初始化脚本代码详见■■Loading:《hugo装修日志06》■■↗

1.和ChatGPT打了数十回合的太极,终于获得了一个可以正常显示的封装代码。

{{ define "main" }}
<link href="/css/pagefind-ui.css" rel="stylesheet">

<div id="search"></div>

<script>
(function() {
    // 动态加载 PagefindUI 脚本
    function loadPagefindUI(callback) {
        var script = document.createElement('script');
        script.src = '/search/pagefind-ui.js';
        script.type = 'text/javascript';
        script.onload = callback;
        document.head.appendChild(script);
    }

    function initializePagefind() {
        var searchElement = document.querySelector('#search');
        if (searchElement) {
            // 清除旧的 PagefindUI 实例
            searchElement.innerHTML = '';

            // 初始化新的 PagefindUI 实例
            new PagefindUI({
                element: "#search",
                showImages: false,
                excerptLength: 3,
                translations: {
                    placeholder: "请小狗搜索员搜索——",
                    clear_search: "删除!",
                    load_more: "让小狗搜索员继续寻找",
                    search_label: "站内检索",
                    filters_label: "文件分类",
                    zero_results: "小狗搜索员没有找到关于 [SEARCH_TERM] 的文件,可能一不小心被丢到地狱去了……",
                    many_results: "小狗搜索员成功找到了 [COUNT] 个关于 [SEARCH_TERM] 的文件!",
                    one_result: "小狗搜索员成功找到了 [COUNT] 个关于 [SEARCH_TERM] 的文件!",
                    alt_search: "小狗搜索员找不到关于 [SEARCH_TERM] 的文件。但是为你带来了另一份关于 [DIFFERENT_TERM] 的文件!",
                    search_suggestion: "小狗搜索员没能找到关于 [SEARCH_TERM] 的文件。可以试试以下方案。",
                    searching: "小狗搜索员正抱着 [SEARCH_TERM] 的委托努力搜索…"
                }
            });
        }
    }

    // 初始化函数
    function init() {
        loadPagefindUI(function() {
            initializePagefind();
        });
    }

    // 初始化 Pagefind
    document.addEventListener('DOMContentLoaded', init);

    // 监听 PJAX 完成事件并重新初始化 Pagefind
    document.addEventListener('pjax:complete', init);
})();
</script>
{{ end }}

2.搜索结果跳转修复,处理方法是直接让pjax监听搜索模块下的链接点击。直接将这段代码复制到原来的search.html里:

<script>
document.getElementById('search').addEventListener('click', function (event) {
  if (event.target.tagName === 'A') {
    event.preventDefault(); // 阻止默认跳转
    let href = event.target.getAttribute('href');
    // 使用 PJAX 处理无刷新跳转
    pjax.loadUrl(href);
  }
});
</script>

说说功能修复

原本初始化代码详见■■Loading:《hugo装修日志04》■■↗

修复方法和搜索功能差不多,但这个在调试过程中又出现了新的bug,全靠ChatGPT老公。

{{ define "main" }}
<script>
(function() {
    let isArtitalkInitialized = false; // 标志位,避免重复初始化

    // 动态加载 Artitalk 脚本
    function loadArtitalk(callback) {
        if (window.Artitalk && !isArtitalkInitialized) {
            callback();
            return;
        }
        
        var existingScript = document.querySelector('script[src="https://unpkg.com/artitalk"]');
        if (existingScript) {
            // 如果脚本已经存在,直接调用回调
            callback();
        } else {
            var script = document.createElement('script');
            script.src = 'https://unpkg.com/artitalk';
            script.type = 'text/javascript';
            script.onload = callback;
            document.head.appendChild(script);
        }
    }

    // 清除旧的 Artitalk 实例
    function clearOldArtitalk() {
        var container = document.getElementById('artitalk_main');
        if (container) {
            container.innerHTML = ''; // 清空容器内容
        }
    }

    // 初始化 Artitalk
    function initializeArtitalk() {
        if (window.Artitalk && !isArtitalkInitialized) {
            new Artitalk({
                serverURL: 'https://api.naturaleki.one',
                appId: "b8EnvWrpGBi9w22Q8e70ZR3R-MdYXbMMI",
                appKey: "goA8d8CvaGk3cnGfZCGyTBAx",
                pageSize: 10,
                motion: 0,
                cssUrl: "/css/eki.css",
                atComment: 0,
            });
            isArtitalkInitialized = true;
        } else if (!window.Artitalk) {
            console.error('Artitalk is not loaded.');
        }
    }

    // 初始化函数
    function init() {
        clearOldArtitalk(); // 清除旧实例
        loadArtitalk(initializeArtitalk);
    }

    // 页面加载完成时初始化 Artitalk
    document.addEventListener('DOMContentLoaded', init);

    // 监听 PJAX 完成事件并重新初始化 Artitalk
    document.addEventListener('pjax:complete', init);
})();
</script>

<div id="artitalk_main"></div>
{{ end }}

友链卡片刷新乱序功能修复

原本脚本详见■■Loading:《hugo装修日志07》■■↗

<script>
function shuffleFriendDivs() {
    // 获取带有 post-content 类的文章内容
    var articleContent = document.querySelector(".post-content");

    // 确保 .post-content 存在
    if (articleContent) {
        // 获取文章内容内部的所有列表项
        var items = articleContent.querySelectorAll(".frienddiv");

        // 将列表项转换为数组
        var itemsArray = Array.from(items);

        // 随机排列数组中的元素
        for (var i = itemsArray.length - 1; i > 0; i--) {
            var j = Math.floor(Math.random() * (i + 1));
            var temp = itemsArray[i].innerHTML;
            itemsArray[i].innerHTML = itemsArray[j].innerHTML;
            itemsArray[j].innerHTML = temp;
        }
    } else {
        console.log("No .post-content element found.");
    }
}

// 页面初次加载时初始化
window.addEventListener('DOMContentLoaded', shuffleFriendDivs);

// 监听 PJAX 完成事件并重新执行脚本
document.addEventListener('pjax:complete', shuffleFriendDivs);
</script>

移除URL的临时参数

引入pjax后,切换页面时网址链接最后会出现类似“?t=1726216255296”的临时参数,我嫌弃它丑,所以让ChatGPT写了一个隐藏临时参数的代码,和pjax.html放在一起。

// 监听 PJAX 事件
$(document).on('pjax:complete', function() {
    // 获取当前 URL
    var url = location.href;
    
    // 如果 URL 包含临时参数,移除它们
    if (url.indexOf('?t=') !== -1) {
        // 生成新的 URL(不包含临时参数)
        var cleanUrl = url.replace(/(\?|&)t=[^&]*/, '');

        // 更新浏览器的历史记录
        history.replaceState(null, '', cleanUrl);
    }
});

添加虚拟进度条

1.在topbar官方文档↗里下载zip包,把解压后的topbar.min.js放到/static/js里。

2.在pjax.html里复制以下代码:

<script src="/js/topbar.min.js"></script>

<script>
  // 修改进度条颜色
  topbar.config({
      barColors: {
          '0': 'rgba(138, 162, 211, 1)', 
          '1.0': 'rgba(138, 162, 211,  1)' 
      }
  })

  document.addEventListener('pjax:send', () => {
      // 显示顶部进度条
      topbar.show();
  })

  document.addEventListener('pjax:complete', () => {
      // 隐藏顶部进度条
      topbar.hide();
  })
</script>

推送,从此博客拥有了和主题颜色一致的美丽加载条!

后记

这次装修真的算是为了一碟醋包了一大盘饺子了,实现aplayer不中断播放后激情更新了一大堆全局歌单,欢迎来听!