评论系统迁移:从 Valine 到 Waline

翻了下之前的折腾记录:

  • 2022 年写《Valine评论异常解决》,LeanCloud 国际版 us-api.leancloud.cn 下线,靠自定义 serverURLs 续命;
  • 2023 年写《Valine折腾再记》,LeanCloud 国际版共享域名 *.avosapps.us 对国内 IP 返回 1020 直接被墙,评论提交必失败,FQ 才能用。

但是,LeanCloud 将于 2027 年 1 月 12 日停止对外提供服务,这下又得折腾了。不过,现在有AI辅助,时代进步的具象化,工具真的听得懂人话了,哈哈。

看了一圈,Waline 是现在公认最省心的方案,而且它后端可以自由选择(LeanCloud / MySQL / PostgreSQL / SQLite),正好可以把数据从 LeanCloud 彻底搬出来。

整体方案

  • 评论 + 文章阅读量:Waline v3(@waline/client@3
  • 后端数据库:Neon Postgres(免费、serverless,和 Vercel 同生态)
  • 部署:Waline 服务端 Deploy 到 Vercel,环境变量连 Neon
  • 前端:Gridea Pro 的 NexT 主题,手动接入 Waline 组件

一、数据迁移(最坑的部分)

问题

从 LeanCloud 导出的数据直接灌进 Neon,发现 wl_comment 表是空的,只有最后一行插入成功。

排查

Neon 的 SQL Editor 是单事务执行的:我把建表语句和 34 条 INSERT 混在一个文件里粘贴,前面任何一句报错(语法不兼容),整个事务就回滚了,最后只有末尾单独运行的那行「侥幸」进去了。

更隐蔽的两个字段问题:

  1. LeanCloud 导出的 insertedAt{"$date": "2020-..."} 这种对象,而 Waline 的 wl_comment.insertedAtTIMESTAMPTZ,需要纯字符串。
  2. pid/rid(父评论、回复对象)在 LeanCloud 里是 objectId 字符串,但 Waline 表里是 INT。直接塞字符串,导入要么报错要么关联错乱。

解决

写了个清洗脚本(Python),做三件事:

  • 拆成「先建表、后插数据」两个文件分别执行,避免单事务整体回滚;
  • {"$date": ...} 提取成 ISO 字符串;
  • 建立「LeanCloud objectId ↔ 数字 id」映射表,把 pid/rid 全部转成对应数字 id。

最终 wl_comment 34 条、wl_counter(阅读量)100 条顺利入库,最后别忘了 ALTER SEQUENCE ... RESTART WITH 重置自增序列。

经验:凡是「导出来少了一半」这种问题,先怀疑单事务回滚 + 字段类型不匹配,别急着怀疑数据库挂了。

二、Vercel + Neon 部署

问题

按官方文档 Deploy 到 Vercel 后,去 /ui/register 注册管理员,直接 500: Not initialized

排查

看 Vercel 日志,报错栈里竟然有 leancloud-storage!说明 Waline 走偏了——它既识别到 LEAN_* 系列环境变量,又没读到正确的 PG_*

关键点:

  • Waline 读的是 PG_*(有下划线)环境变量,不是 POSTGRES_* / PGHOST
  • 我之前图方便把 LeanCloud 的 LEAN_* 变量也填进了 Vercel,干扰了判断;
  • 而且 Vercel 的 Environment Variables 得确认覆盖到 Production 环境。

解决

  • 删掉所有 LEAN_* 变量;
  • 只保留 PG_HOST / PG_PORT / PG_USER / PG_PASSWORD / PG_DATABASE / PG_SSL
  • 注意 PG_PORT=5432PG_SSL=true(Neon 强制 SSL);
  • 重新 Deploy,注册成功。

经验:Not initialized 基本就是「没连上数据库 / 读错环境变量」。Waline 默认优先 LeanCloud,一旦有 LEAN_* 残留就会去连 LeanCloud 然后 500。干净的环境变量是第一步。

三、Gridea Pro NexT 主题接入

这部分坑最多,因为 Gridea Pro 的主题机制和 Hexo 原版 NexT 不一样。

坑 1:白名单机制,模板读不到配置

评论区一直显示的是旧的 Valine,而不是 Waline。

Gridea Pro 有个白名单:只有 config.jsoncustomConfig 声明过的字段,才会传给模板。原版 NexT 只声明了 valine,所以即便我在 theme.json 里写了 waline: truewalineServerURL,模板里 site.customConfig.waline 依然是 undefined

解决:在主题的 config.json 的「评论」分组里补上 waline(开关)和 walineServerURL(地址)两个声明,模板才能读到。

坑 2:评论列表一直 loading(不是数据库!)

前端能打开,但评论列表一直转圈。F12 一看:后端 /api/comment?path=/post/hmjqsl/ 返回 200,数据正常,但浏览器控制台报错 Cannot read properties of null (reading 'insertBefore'),来自 waline.umd.js 的 Vue 挂载阶段。

根因:@waline/client@3/dist/waline.umd.js 这个 UMD 构建,在 Gridea 主题这种「同步脚本 + 复杂 DOM」场景下 mount 会失败(parent 节点为 null)。

解决:改用 ESM 模块方式加载:

<script type="module">
  import { init } from 'https://unpkg.com/@waline/client@3/dist/waline.js';
  init({ el: document.getElementById('waline'), serverURL: '...', path: window.location.pathname });
</script>
  • type="module" 天然 deferred,DOM 加载完才执行;
  • 直接传 el: 元素 而不是 selector 字符串,避开解析歧义。

坑 3:阅读量接口名和元素 class 都变了

Waline v3 的阅读量:

  • 接口是 /api/article(不是 v2 的 /api/pageview);
  • 页面上要放 <span class="waline-pageview-count">0</span>,并初始化时开 pageview: true
  • 旧主题的 leancloud-visitors-count 元素得改名。

坑 4:关于页默认没评论

/post/about/ 是独立模板 about.ejs 渲染的,原本既没评论块也没阅读量。手动在 about.ejs 里引入 comment 块、加上 waline-pageview-count,并把评论区移进内容卡片内部、去掉独立圆角,视觉上更和谐。

坑 5:CDN 与资源

  • jsDelivr 在国内被墙,早期 waline.js(ESM)加载不出来;改用 unpkg,或把 waline.js / waline.css 本地化到 images/waline/
  • 注意:Waline v3 主文件 waline.jsESM,普通 <script src> 会报 Unexpected token 'export';要么用 waline.umd.js,要么用 <script type="module"> 引入 ESM 版。

UI 适配

评论区背景色直接抄主题的 .bg-color#fffffff2(白底带透明度),和文章卡片融为一体;主题色用 NexT 蓝 #49b1f5,字号 16px、行高 1.8 对齐正文。

经验总结

  1. Waline 后端选型是关键:直接上 Neon Postgres / MySQL,一劳永逸。
  2. 数据迁移先看字段类型:objectId→INT、日期对象→字符串,清洗脚本比手动改省事。
  3. Vercel 环境变量要干净:只留 PG_*,删掉 LEAN_*,否则 Not initialized
  4. 前端 loading 先看 Network 和 Console:后端 200 就不是数据库问题,八成是前端 mount / CDN / 路径。
  5. Gridea Pro 主题有白名单config.json 不声明,theme.json 写了也白搭。

ps: 这次折腾下来,最大的感受是——能用脚本清洗的脏数据,千万别手搓能看日志定位的 bug,别靠猜。剩下的,就交给时间检验吧。


更新于 2026年07月25日