我的博客评论系统是 Hugo + Waline,数据一直托管在 LeanCloud。几个月前,LeanCloud发邮件告诉我“快要停服了”,但我转头就把这事忘了。最近翻阅友链,辛未羊在评论区说waline托管平台要关了,我才想起来这码事。
LeanCloud、Vercel托管、Waline评论管理,只有建站的时候才配置过。有些东西平时感觉不到,真要搬起来还挺麻烦。
起因
Waline 本身只是一个评论服务端,存储后端其实可以换。LeanCloud 只是其中一个选项。
官方支持的后端大概有:
- MongoDB
- MySQL / TiDB
- PostgreSQL
- SQLite
- 腾讯云 CloudBase
- GitHub
一开始我也纠结过要不要趁机换个评论系统,比如 Giscus、Twikoo、Artalk 什么的。但想了想,换评论系统意味着历史评论、回复关系、用户数据都要重新处理,迁移成本太高了。而且我只需要把数据从 LeanCloud 挪走,没必要连 Waline 一起换掉。
所以最后决定:继续用 Waline,只换数据库。
为什么选 MongoDB
MongoDB 最大的优势是:LeanCloud 的 BSON 备份可以直接恢复进 MongoDB。
也就是说,不用把评论一条条转成 SQL,不用重新设计表结构,Comment、Users、Counter 这些数据基本能原样搬过去。
而如果选 MySQL/PostgreSQL,虽然 Waline 官方提供了转换工具,但主要处理 Comment,Users、Counter、Tag 这些附加数据往往还得自己写脚本。
所以我的选择很简单:
哪个最省事、最不容易丢数据,就选哪个。
导出数据
LeanCloud 控制台里可以导出两种东西:
- JSON 格式:按 Class 导出,适合转换后导入 SQL 系数据库
- BSON 格式:数据库备份,适合直接导入 MongoDB
我一开始导了 JSONL,后来发现还能导 BSON,果断又导了一份 BSON。官方说BSON导出更快,但实际上官网下载有问题,等半天还显示导出中,邮件发来的倒是可以直接下载了。
最后拿到手的文件是:
Comment.bson.gz
Users.bson.gz
Counter.bson.gz
评论 139 条,访问量 351 条,用户 7 个。不多,但也不少。
导入过程:并没有想象中顺利
我本来以为 BSON 导出来之后,直接 mongorestore 就完事了。
结果发现一个坑:
Comment 的 _id 不全是 ObjectId。
有一部分是 "187"、"186" 这种普通字符串,另一部分才是 MongoDB 的 ObjectId。而评论之间的 pid、rid 又互相引用这些旧 ID。
如果直接导入,Waline 按 objectId 查评论时,字符串 ID 不是合法 ObjectId,回复层级就会坏掉。
所以最后写了个小脚本:
- 给旧的字符串 ID 生成新的 ObjectId;
- 建立旧 ID → 新 ID 的映射;
- 同步改写每条评论的
pid、rid; - 再导入 MongoDB。
导入完成后还检查了一遍:
- 所有
pid/rid都能找到对应评论; - 所有
user_id都能找到对应用户; - 评论、用户、访问量数量都一致。
Vercel 的连环坑
第一个坑:ERR_REQUIRE_ESM
部署到 Vercel 后,一访问就 500:
Error [ERR_REQUIRE_ESM]: require() of ES Module
/var/task/node_modules/@exodus/bytes/encoding-lite.js
原因是 Waline 依赖的 jsdom@29 底层用了 ESM-only 的 @exodus/bytes,而 Vercel 的 Node 运行时不支持直接 require() 它。
一开始我以为升级 Node.js 版本到 24.x 就行,结果没用。
只覆盖 html-encoding-sniffer 也不行,因为 whatwg-url 也 require 了同一个 ESM 包。
最后最有效的办法是:把 jsdom 降到 26.1.0,整个依赖链就回到 CommonJS 了。
{
"overrides": {
"jsdom": "26.1.0",
"html-encoding-sniffer": "4.0.0"
}
}
第二个坑:MathJax 字体文件丢失
ESM 问题解决后,又变成:
Cannot find module
'/var/task/node_modules/@mathjax/mathjax-newcm-font/mjs/chtml/default.js'
原因是 Vercel 打包 Serverless 函数时,自动文件追踪漏掉了 MathJax 的字体文件。
解决方法是在 vercel.json 里显式把字体文件包含进去:
"config": {
"includeFiles": [
"node_modules/@mathjax/mathjax-newcm-font/**/*",
"node_modules/mhchemparser/**/*",
"node_modules/ip2region/data/**"
]
}
第三个坑:登录管理后台显示账号密码错误
服务终于能打开了,但登录管理后台时一直提示“账号密码错误”。
我一度以为迁移把密码弄坏了,甚至准备直接重置密码。
后来我用接口测了一下:
POST /token
返回值并不是密码错误,而是:
{"errno":500,"errmsg":"ResourceRequest timed out"}
这是 MongoDB 连接超时。
原因很简单:MongoDB Atlas 的白名单里只加了我本机的 IP,但 Waline 跑在 Vercel 的 Serverless 环境里,Vercel 的服务器 IP 是动态的,根本连不上数据库。
解决办法也简单:在 Atlas Network Access 里加上:
0.0.0.0/0
允许所有 IP 访问。数据库本身有用户名密码保护,个人博客这样用问题不大。
最终结果
折腾了一晚上,终于跑通了。
现在的架构是:
Hugo 博客
↓
Waline(Vercel Serverless)
↓
MongoDB Atlas
历史评论、回复层级、访问量、用户数据都还在。
Waline 环境变量大概是这样的:
MONGO_DB=waline
MONGO_USER=xxx
MONGO_PASSWORD=xxx
MONGO_HOST=["cluster0-shard-00-00.xxx.mongodb.net","cluster0-shard-00-01.xxx.mongodb.net","cluster0-shard-00-02.xxx.mongodb.net"]
MONGO_PORT=[27017,27017,27017]
MONGO_REPLICASET=atlas-xxx-shard-0
MONGO_AUTHSOURCE=admin
MONGO_OPT_SSL=true
一些感受
这次迁移本身不难,难的是那些“看起来毫不相关”的连锁坑:
- Node 运行时版本;
- 依赖包从 CJS 变 ESM;
- Vercel 打包漏文件;
- 数据库白名单没放行 Serverless。
总结:
- 先备份,再折腾;
- 能用 BSON 直导就不要手动转 CSV;
- Serverless 连数据库,第一时间检查 IP 白名单;
- 遇到登录类报错,先看接口真实返回,别被前端提示误导。
记录一下,下次再遇到类似迁移,至少知道该先看哪里。

何时一樽酒,重与细论文。