背景:一条硬约束

评论系统是博客的灵魂。但我有条硬约束:不想维护一个评论系统的后端服务,不想半夜被报警叫起来修评论数据库。我要的是一个”部署之后就能忘掉”的方案。

AI 给了三个选项,各有取舍:

  • Giscus:评论数据存 GitHub Discussions,零后端。缺点:用户必须登录 GitHub 才能评论,普通访客门槛太高;数据不在自己手里。
  • Twikoo:基于 serverless 部署,数据存自己的 MongoDB,支持匿名评论,有管理面板。缺点:AI 推荐的管理员登录方案(Netlify Identity + Git Gateway)早在 2025 年 2 月就被官方废弃;而且 Twikoo 开发者活跃度在下降,最后 release 停在较早的版本。
  • Waline:跟 Twikoo 很像,但社区更活跃、文档更全、跟 Twikoo 数据兼容可以直接迁移、有独立的注册页 /ui/register,不依赖第三方认证。

从最开始只实现了Giscus方案,后续修改为了Twikoo方案(此时用的MongoDB),最终选了 Waline + Giscus 双 Tab:默认 Waline(游客免登录,降低评论门槛),可切 Giscus(给有 GitHub 账号的深度用户)。Waline 挂了还有 Giscus 兜底。

选型本身很顺利,坑全在后面的迁移和部署。这一篇就是三段翻车记录:Netlify 404 连猜四轮、Vercel 打转两天、最后靠官方文档半小时救场。

事故一:Netlify 上 404,AI 连猜四轮

背景:把 Waline 的 serverless 函数部署到 Netlify 上新建的 lublog-comments 站点(把 Twikoo 时代的旧仓库改关联过来)。部署完访问注册页 /.netlify/functions/comment/ui/register,一直 404。

时间线(每一轮都是”改完部署一下就好了”):

轮次AI 的笃定判断结果
1一定是因为 JWT_TOKEN 没设置,注册页才不暴露部署,404
2MONGO_HOST 格式不对,是 JSON 数组不是逗号分隔部署,404
3变量被 Netlify 强制成 Secret 了,改成 All scopes部署,404
4一定是函数路径前缀没剥离,改 comment.js部署,404

代价:Netlify 团队免费额度 300 credits/月,每次生产部署约 15 credits,被反复烧。核心问题不是 AI 不会分析,而是它没证据却一副我懂了的样子,让我误以为每次都是最后一次,放心去点部署。

转机:用 Netlify CLI 拉站点配置,发现残留一个 Twikoo 时代的 MONGODB_URI,指向已经停用的旧集群 cluster0。删掉它、又加了路径前缀剥离,重新部署,依然 404。

真根因:官方 Waline Netlify starter 用的 serverless-http 适配器,跟当前 Netlify Functions 运行时不兼容。函数确实被调用了,但请求无法被翻译成 Waline 能路由的格式,于是所有路由(含注册页)全军覆没。前面四轮全猜错了。

结论:迁 Vercel。那里是 @waline/vercel 的原生环境,没有适配层,一次部署大概率就能通。

事故二:Vercel 上打转一整天,参数改了个寂寞

以为换个平台就”一次通”,结果又打转整整一天,直到翻开官方文档才成功。

坑 1:盲目用 latest,撞上 ESM 重构

package.json 里写 "@waline/vercel": "latest",部署后注册页能打开,但一写库就崩:

Error [ERR_REQUIRE_ESM]: require() of ES Module
/var/task/node_modules/@mdit/plugin-katex/dist/index.js

@waline/vercel 在 1.41 版本做了一次 ESM 重构,内部直接发布未编译源码,还依赖了纯 ESM 的 @mdit/plugin-katex。旧版入口 index.cjs 是 CommonJS,require() 一个 ESM 包直接崩。官方文档教程对应的其实是 1.31.x 时代的老写法。我没核对版本就写 latest,等于主动踩进了重构后的雷区。

坑 2:环境变量打转

我试的操作结果
MONGO_URL 完整连接串Waline 根本不读,报 No valid storage found
删掉零散 MONGO_* 只留 URL启动直接崩
MONGO_* 全部加回去恢复能启动但连不上
MONGO_AUTHSOURCE=admin连库时间从 5 分钟变 30 秒,还是不成功
MONGO_OPT_SSL=true同样不成功
MONGO_PORT27017 改成数组 [27017,27017,27017]端口从 :2,:7,:0 修回 :27017,还是连不上

最坑的是 MONGO_PORTthink-mongo 期望端口是数组,我把字符串 "27017" 传进去,它按字符拆,给了三个 host 各一个 270 当端口。日志里那行 host:2, host:7, host:0 我一度以为只是日志被截断,直到看完整日志才发现是真实端口。

坑 3:调超时,纯属治标

连库一直挂起,又去调超时:generic-pool 的 acquireTimeoutMillis 从 3 秒改成 30 秒;函数加 maxDuration: 90;测试函数里写 Promise.race 50 秒强制返回。效果是错误形式变了,失败的本质没变。

最后翻出 Vercel Hobby 计划的文档才明白:函数 maxDuration 默认只有 10 秒,而 MongoDB Atlas 握手要 30 秒以上。这是平台套餐和数据库的物理组合问题,不是任何代码参数能解决的。我调整的所有超时,都是在给一个不可能的组合续命。

真相:两层不兼容

把 Vercel 函数日志完整翻出来,才拼出全貌:

  1. 驱动不兼容@waline/vercel 底层 think-mongo 是 2018 年的包,锁死 mongodb@3.5.9 驱动,而我的 Atlas 集群是 MongoDB 8.0。老驱动与 8.0 握手直接挂起。用 npm overrides 把驱动升到 6.x,又暴露 ObjectID 被移除、端口数组、useUnifiedTopology 弃用警告等一串兼容伤。
  2. 平台限制:Vercel Hobby 函数 maxDuration 10 秒,MongoDB 握手需要 30 秒以上。即使驱动兼容,这个组合也跑不通。

两层任何一层都判了死刑。而我在环境变量和超时上打转,是因为这两层得把完整日志、版本号、官方文档摊一起看才看得出来,不是看一眼报错就能想到的。

修复:放下执念,走官方路线

实在没辙了,我翻开 Waline 官方部署文档(waline.js.org/guide/deploy/vercel.html),发现官方在 Vercel 上演示的数据库方案根本不是 MongoDB,而是 Neon(PostgreSQL),Vercel 的托管数据库:

  1. 点文档里的 Vercel 一键部署按钮(官方 example 模板)
  2. Vercel → Storage → Create Database,选 Neon,自动注入 POSTGRES_HOST 等 4 个变量
  3. 在 Neon 的 SQL Editor 里执行官方 waline.pgsql 建表 SQL
  4. Redeploy
  5. 打开 /ui/register 注册,首个用户自动成为管理员

半小时完成,一次成功。原因很清楚:Neon 是 Vercel 托管数据库,连接走内部优化,握手小于 1 秒,10 秒的 maxDuration 绰绰有余。环境变量也是集成自动注入的,连手动配变量这步都省了。

教训

  1. AI 说已经找到根因时,先要证据。让它贴响应头、函数日志、变量实际值,而不是一句结论。证据不到位,结论就是猜测。
  2. 改完再部署要算账。部署烧 credits 或费用,先做一次完整的根因分析,把怀疑的点排一排,先试最像的,别每次只堵一个假设去试。
  3. 能免费验证的,绝不先部署。环境变量、重定向、构建配置都能在部署前用 CLI 或后台核对,不需要用生产部署当试错成本。
  4. 反复调参 3 轮以上还没好,先查官方文档。环境变量、超时这类参数,调一百次也只是在一个错误方向上微调。
  5. 版本兼容性是最容易被忽略的根因。2018 的库连 2024 的数据库,报的错往往千奇百怪(404、超时、instanceof、无效端口),没一个是字面意思。核对版本号、看官方支持的组合,比猜环境变量快得多。
  6. 平台限制是硬约束。Vercel Hobby 的 maxDuration=10s 不是 bug,是套餐功能。在这种硬约束下,任何让连接更快的代码努力都是徒劳,应该换方案(换数据库、升套餐、换平台),而不是换参数。
  7. 适配器或运行时类问题,优先用原生支持的环境。Waline 是 @waline/vercel,在 Vercel 原生跑;硬塞进 Netlify 的 serverless-http 适配层,就是给自己埋坑。
  8. AI 过度积极也是病。Netlify 篇是过度自信,Vercel 篇是过度积极:把猜的东西一条条写成能直接执行的参数改动,让你在看起来在推进的循环里烧掉时间。对治方法只有一条:停下来,看证据,查文档。