跳过正文

中文博客目录跳转失败

·279 字

问题描述
#

Hugo 会自动为每篇博客文章生成目录(Table of Contents),目录中的每个标题都应该支持点击跳转到对应的锚点位置。这是博客阅读体验中非常基础且重要的功能。

几天前我在浏览自己的博客时发现:英文页面的目录跳转正常,但所有中文页面的目录点击后毫无反应。浏览器地址栏中的 URL 会短暂变化,但页面并不会滚动到目标标题位置。

排查过程
#

第一步:确认问题是否与主题相关
#

首先怀疑是 Hugo 主题配置问题。我切换回主题默认配置,关闭了所有自定义修改,但问题依旧。排除了主题配置的原因。

第二步:检查是否是个别文章的问题
#

我测试了多篇中文文章和英文文章,发现规律非常稳定:所有英文标题跳转正常,所有中文标题跳转失效。这说明问题与标题的语言(字符编码)有关,而非文章内容本身。

第三步:二分法定位引入问题的代码
#

我开始逐一注释掉页面中最近添加的功能模块,用二分法定位问题源:

  1. 先注释掉所有第三方 JS 库 → 问题依旧
  2. 保留基础库,逐个恢复功能模块 → 当恢复 APlayer 音乐播放器时,问题复现
  3. 再次注释掉 APlayer → 问题消失

至此确认:APlayer 是导致中文锚点跳转失效的直接原因

第四步:确认 APlayer 版本与来源
#

我去 APlayer 官方 GitHub 仓库和 CDN 下载了最新的纯净版文件进行替换测试,发现问题依旧存在。这说明问题存在于官方源码中,并非我下载的版本被第三方篡改。

第五步:查看控制台错误与源码分析
#

打开浏览器开发者工具(F12),点击中文目录链接时,控制台没有报错,但页面没有滚动。查看 APlayer 源码后,在其中发现了如下代码段:

// 这段代码来自 APlayer 中内嵌的 smoothScroll 插件
var n = function(e) {
    if (!e.defaultPrevented) {
        e.preventDefault();
        location.hash !== this.hash && window.history.pushState(null, null, this.hash);
        var n = document.getElementById(this.hash.substring(1));
        if (!n) return;
        t(n, 500, function(e) {
            location.replace("#" + e.id)
        })
    }
};

问题就出在这里this.hash 返回的是 URL 编码后的字符串(例如中文标题 #中文 会被编码为 #%E4%B8%AD%E6%96%87),而 document.getElementById() 接收的参数是解码前的字符串,自然找不到对应的 DOM 元素,导致跳转静默失败。

第六步:查阅资料确认问题本质
#

搜索后发现这是一个2018 年就已被发现但至今未被官方修复的陈年 Bug

  • APlayer 内嵌的 smoothScroll 插件在处理锚点跳转时,默认只对英文/ASCII 字符有效
  • 中文等非 ASCII 字符会被转为 Unicode/百分比编码,导致 getElementById 查找失败
  • 官方维护停滞,社区中已有大量用户遇到相同问题

原因分析
#

为什么只有中文出问题?
#

  • 英文标题的 URL 编码后与原字符串一致(如 #hello#hello),getElementById("hello") 可以正常工作
  • 中文标题的 URL 编码后会变成 #%E4%B8%AD%E6%96%87getElementById("%E4%B8%AD%E6%96%87") 无法匹配到 id="中文" 的元素

根本原因
#

APlayer 引入的 smoothScroll 插件在处理 hash 时,没有对 URL 编码进行解码,直接使用编码后的字符串去查询 DOM 元素,导致匹配失败。

相关 issue 参考:

解决方案
#

修改 <博客主目录>/static/js/aplayer/ 目录下的 APlayer.min.js 文件。

定位到包含 defaultPrevented 的代码段(通常在文件中间位置),将原始代码替换为以下版本(增加 decodeURIComponent 处理):

javascript

if (!e.defaultPrevented) {
    e.preventDefault();
    // 使用 decodeURIComponent 解码 URL 编码的 hash
    decodeURIComponent(location.hash) !== decodeURIComponent(this.hash) && 
        window.history.pushState(null, null, decodeURIComponent(this.hash));
    var n = document.getElementById(decodeURIComponent(this.hash).substring(1));
    if (!n) return;
    t(n, 500, function(e) {
        location.replace("#" + e.id)
    })
}

替换说明
#

原始代码修改后代码
location.hash !== this.hashdecodeURIComponent(location.hash) !== decodeURIComponent(this.hash)
window.history.pushState(null, null, this.hash)window.history.pushState(null, null, decodeURIComponent(this.hash))
document.getElementById(this.hash.substring(1))document.getElementById(decodeURIComponent(this.hash).substring(1))

成品文件
#

如果不想手动修改,可以直接使用社区已修复的版本:

验证结果
#

替换文件后,清除浏览器缓存并刷新页面:

  • ✅ 中文目录点击正常跳转
  • ✅ 英文目录点击正常跳转
  • ✅ 地址栏 URL 显示为解码后的中文(可读性更好)
  • ✅ APlayer 播放器所有功能正常

总结与反思
#

经验教训
#

  1. 第三方库引入需谨慎:APlayer 作为一个音乐播放器,其内嵌的 smoothScroll 插件对中文支持不完善,这种"附带功能"往往容易被忽略
  2. 国际化/多语言测试不可少:功能测试时不仅要测英文环境,中文等非 ASCII 字符场景同样重要
  3. 老旧依赖需警惕:APlayer 自 2019 年后维护频率极低,此类基础 Bug 长期未被修复,使用时需自行 patch

这些问题都是 2018 年就暴露出来的老问题,官方至今未修复,建议有需要的开发者自行修改。


参考文章: