问题描述#
Hugo 会自动为每篇博客文章生成目录(Table of Contents),目录中的每个标题都应该支持点击跳转到对应的锚点位置。这是博客阅读体验中非常基础且重要的功能。
几天前我在浏览自己的博客时发现:英文页面的目录跳转正常,但所有中文页面的目录点击后毫无反应。浏览器地址栏中的 URL 会短暂变化,但页面并不会滚动到目标标题位置。
排查过程#
第一步:确认问题是否与主题相关#
首先怀疑是 Hugo 主题配置问题。我切换回主题默认配置,关闭了所有自定义修改,但问题依旧。排除了主题配置的原因。
第二步:检查是否是个别文章的问题#
我测试了多篇中文文章和英文文章,发现规律非常稳定:所有英文标题跳转正常,所有中文标题跳转失效。这说明问题与标题的语言(字符编码)有关,而非文章内容本身。
第三步:二分法定位引入问题的代码#
我开始逐一注释掉页面中最近添加的功能模块,用二分法定位问题源:
- 先注释掉所有第三方 JS 库 → 问题依旧
- 保留基础库,逐个恢复功能模块 → 当恢复 APlayer 音乐播放器时,问题复现
- 再次注释掉 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%87,getElementById("%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.hash | decodeURIComponent(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 播放器所有功能正常
总结与反思#
经验教训#
- 第三方库引入需谨慎:APlayer 作为一个音乐播放器,其内嵌的 smoothScroll 插件对中文支持不完善,这种"附带功能"往往容易被忽略
- 国际化/多语言测试不可少:功能测试时不仅要测英文环境,中文等非 ASCII 字符场景同样重要
- 老旧依赖需警惕:APlayer 自 2019 年后维护频率极低,此类基础 Bug 长期未被修复,使用时需自行 patch
这些问题都是 2018 年就暴露出来的老问题,官方至今未修复,建议有需要的开发者自行修改。
参考文章:
