Hexo NexT 评论系统长期失效的排查与修复
这个博客最初在 Windows 上维护,后来迁移到 Linux。迁移之后,文章正文、静态资源和部署流程都能正常工作,唯独评论系统长期无法恢复。
问题最令人困惑的地方在于:NexT 配置文件里明明填写过 Gitalk、Twikoo 和 Utterances 的配置,生成过程也没有直接报告评论相关错误,但文章末尾始终是一片空白。由于故障恰好发生在系统迁移之后,很容易把原因归结为 Windows 与 Linux 的路径、换行符或 Node.js 环境差异。
最终排查表明,操作系统迁移只是时间上的巧合。真正的问题分为两层:
- 评论配置长期处于相互矛盾的“半启用”状态;
- 切换到 Utterances 后,NexT 的自定义加载事件又发生了脚本时序竞争。
后者才是“页面源码里明明有评论代码,浏览器里却什么都没有”的直接原因。
完成配置并重新生成后,检查
public/cycle.html,可以看到评论容器、Utterances
配置和加载脚本全部存在:
1 | <div class="comments utterances-container"></div> |
线上页面也包含完全相同的内容,https://utteranc.es/client.js
返回 HTTP 200。这说明:
- Hexo 已经读取评论配置;
- NexT 已经注入评论容器;
- 修复后的静态文件已经部署;
- Utterances 官方脚本可以访问。
如果只检查生成产物,似乎所有环节都正常。但“脚本标签存在”并不等于“脚本按照预期执行”,所以排查必须从构建阶段进入浏览器运行时。
使用浏览器运行时取证
通过无界面 Chrome 和 Chrome DevTools Protocol 读取实际页面状态,得到如下结果:
1 | { |
这段结果非常关键:
.utterances-container确实存在;- 容器内部为空;
- 页面没有创建任何 Utterances iframe;
- 浏览器只看到了 NexT 的包装脚本,没有插入官方
client.js。
因此问题不在 CSS,也不在 GitHub Issue 映射,更不是 iframe 被主题遮挡。加载流程根本没有走到创建 iframe 的阶段。
根因:defer 脚本和 page:loaded
事件发生竞争
NexT 的 Utterances 脚本并不立即加载评论,而是先监听一个主题自定义事件:
1 | document.addEventListener('page:loaded', () => { |
page:loaded 由 NexT 的 utils.js
触发。原逻辑会根据 document.readyState
决定立即派发事件,或者只等待一次状态变化:
1 | const onPageLoaded = () => document.dispatchEvent( |
问题在于,utils.js 位于页面较前的位置,而 Utterances
包装脚本位于文末,两者都使用
defer。在某些加载顺序下会出现如下过程:
- HTML 解析接近结束,
document.readyState进入interactive; - 前面的
utils.js执行并立即派发page:loaded; - 文末的 Utterances 脚本稍后才执行并注册监听器;
- 事件不会为后来注册的监听器自动重放;
- 评论容器永远保持空白。
如果前面还存在加载缓慢的第三方 defer
脚本,执行顺序会变得更加脆弱。这也解释了为什么问题看起来像是“有时正常、有时异常”,以及为什么仅凭静态
HTML 很难发现。
最终在 NexT 的
themes/next/layout/_third-party/comments/utterances.njk
模板中加入一个独立加载器。它会在浏览器解析到文末时立即执行,同时仍监听
page:loaded,用于兼容 PJAX 页面切换。
核心实现如下:
1 | (() => { |
这个实现有几个重要设计点。
- 不再等待可能已经错过的事件
注册监听器后立即调用一次 loadUtterances()。即使 NexT
已经派发过 page:loaded,首次打开文章时仍会加载评论。
- 不依赖 NexT 的运行时对象
加载器直接读取页面中的 JSON 配置节点,不要求
CONFIG、NexT.utils
或其他主题脚本已经完成初始化。因此,即使某个前置第三方脚本加载缓慢,评论也不会被它阻塞。
- 使用幂等保护避免重复加载
通过 data-utterances-loading 标记容器。立即调用和
page:loaded 事件即使先后到达,也只会插入一次
client.js。
- 保留 PJAX 兼容性
首次打开首页时可能没有评论容器,但监听器仍然存在。通过 PJAX
进入文章后,新的 page:loaded 会再次检查容器并加载评论。
- 失败必须对用户可见
原故障最大的麻烦是“静默失败”:页面只留下一块空白。修复后的加载器给
script
添加了错误处理。如果官方脚本加载失败,会显示错误提示和 GitHub Issues
备用链接,而不是继续假装一切正常。
1 | script.addEventListener('error', () => { |
这次故障从表面上看只是“文章末尾没有评论框”,实际横跨了 Hexo 数据模型、NexT 注入机制、GitHub App、第三方脚本加载以及浏览器事件时序。
最终有效的排查路径是:先消除配置矛盾,再确认静态生成结果,最后进入真实浏览器观察运行时
DOM。修复的关键也不是简单地再写一次
enable: true,而是让评论加载逻辑摆脱容易丢失的初始化事件,并为重复执行和外部失败做好保护。
从此,评论区终于不再是那个“配置里存在、源码里存在、页面上却不存在”的幽灵组件。