VitePress博客从Valine迁移到Waline:部署、接入与数据迁移
很久没看博客的留言了,最近登录一下,发现之前用的leancloud已经要停服了

决定来迁移一下,把相关的数据迁移到一个新的留言系统,经过调研,最终决定迁移到Valine。
本文涉及Waline 客户端、Vercel 服务端和 Neon PostgreSQL。内容从账号和项目创建开始,依次完成数据库初始化、VitePress 接入、LeanCloud 历史数据转换、导入校验和上线检查。即使没有历史数据,也可以只使用前半部分完成一个新的 Waline 评论系统。
完成后会得到什么
最终调用关系如下:
VitePress 博客
│
│ @waline/client
▼
Vercel 上的 Waline 服务端
│
│ PostgreSQL 环境变量
▼
Neon PostgreSQL这个组合不会改变静态博客的部署方式。博客页面只加载评论组件,评论查询、提交和管理由 Waline 服务端处理,数据存储在独立的 PostgreSQL 数据库中。
完整实施分为四个阶段:
- 在 Vercel 创建 Waline 服务端项目。
- 创建 Neon PostgreSQL 数据库并初始化 Waline 表结构。
- 在 VitePress 中接入
@waline/client。 - 将 LeanCloud 导出的评论转换并写入 Neon。
先完成前端接入和空数据库验证,再迁移历史数据。这样可以分别定位客户端、服务端和数据层的问题。
准备工作
需要准备:
- 一个 GitHub 账号,用于登录 Vercel 和保存 Waline 模板仓库。
- 一个 Vercel 账号。
- 一个 Neon 账号;如果从 Vercel Marketplace 创建,控制台会引导完成授权。
- 一个可以修改代码和环境变量的博客项目。
- 如需迁移历史评论,准备 LeanCloud 导出的 CSV 或 JSON 文件。
本文示例使用 pnpm、VitePress 1.1.x 和 Vue 3.4.x。其他 Vue 静态站点的服务端和数据库步骤相同,客户端挂载方式按对应框架调整。
第一步:在 Vercel 创建 Waline 项目
打开 Waline 官方 Vercel 部署指南,点击页面中的 Vercel 部署按钮。
Vercel 会基于 Waline 模板创建一个新的 Git 仓库和项目:
- 使用 GitHub 登录 Vercel。
- 输入项目名,例如
my-blog-waline。 - 选择自己的 GitHub 账号或组织作为仓库归属。
- 点击
Create,等待首次部署完成。 - 部署完成后点击
Go to Dashboard进入项目控制台。
首次部署只代表 Waline 服务端代码已经运行。数据库创建和表结构初始化完成后,评论 API 才能正常读写数据。
Vercel 每次部署都会生成一个唯一的 Deployment URL。后续应在 Settings → Domains 中确认项目的 Production Domain,并让博客客户端使用这个稳定地址。带有部署哈希和账号名的预览地址可能受到 Deployment Protection 限制。
第二步:创建 Neon PostgreSQL 数据库
方式一:从 Vercel Storage 创建
这是 Waline 官方部署指南当前提供的默认流程:
- 在 Waline 项目控制台进入
Storage。 - 点击
Create Database。 - 在
Marketplace Database Providers中选择Neon。 - 接受 Neon 授权,选择计划、区域和配额。
- 输入数据库名称并完成创建。
- 确认数据库已经连接到当前 Waline 项目。
Vercel Marketplace 会把数据库连接变量添加到项目中。变量名称和数量可能随集成版本变化,可以在 Settings → Environment Variables 中确认,不要把这些变量复制到博客前端。
接着点击数据库的 Open in Neon,在 Neon 控制台中进入 SQL Editor。打开 Waline 仓库中的 PostgreSQL 初始化脚本 waline.pgsql,复制完整 SQL 并执行,用于创建评论、用户和计数等表。
执行成功后回到 Vercel:
- 进入
Deployments。 - 对最新的生产部署执行
Redeploy。 - 等待状态变成
Ready。 - 访问 Production Domain,确认可以打开 Waline 服务端页面。
Vercel 环境变量只会在新部署中生效,因此数据库创建后必须重新部署。
方式二:连接已经创建的 Neon 数据库
如果已经在 Neon 独立创建了项目,可以参考 Neon 官方的 Vercel 手动连接指南 获取连接信息:
- 在 Neon 项目首页点击
Connect。 - 选择需要使用的 branch、database 和 role。
- 复制 PostgreSQL connection string。
- 打开 Vercel 项目的
Settings → Environment Variables。 - 将连接串中的信息拆分成 Waline 支持的 PostgreSQL 环境变量。
- 根据实际需要勾选 Production、Preview 和 Development 环境。
- 保存变量并重新部署 Waline。
Waline 官方文档列出的变量如下:
PG_DB=数据库名
PG_USER=角色名
PG_PASSWORD=角色密码
PG_HOST=Neon主机名
PG_PORT=5432
PG_SSL=true也可以使用对应的 POSTGRES_DATABASE、POSTGRES_USER、POSTGRES_PASSWORD、POSTGRES_HOST、POSTGRES_PORT 和 POSTGRES_SSL 别名。以 Waline 服务端环境变量文档 为准。
Neon 的通用指南使用 DATABASE_URL 连接普通 Vercel 应用,但 Waline 当前文档没有把 DATABASE_URL 列为 PostgreSQL 配置项。手动配置时不要只添加一个通用连接串。
然后仍需在 Neon SQL Editor 中执行 Waline 的 waline.pgsql。连接串属于服务端凭据,只能保存在受控的环境变量或本地密钥文件中。
第三步:验证服务端并注册管理员
先访问:
https://你的-production-domain/页面能够打开后,再访问:
https://你的-production-domain/ui/register第一个注册的用户会成为 Waline 管理员,可以在 /ui 中管理评论。建议在正式开放评论前完成管理员注册。
还可以直接请求一个空页面的评论接口:
https://你的-production-domain/api/comment?path=%2Fwaline-test预期响应是 Waline JSON,而不是 Vercel 登录页、HTML 错误页或数据库异常。此时再进入博客客户端接入阶段。
第四步:在 VitePress 中接入 Waline
项目使用 VitePress 1.1.x 和 Vue 3.4.x。安装客户端时需要同时考虑 Waline 与 Vue 的版本兼容性,本次最终使用的解析版本为 @waline/client@3.2.7。
pnpm add @waline/client@3.2.7这里使用确定版本是为了与 Vue 3.4 保持兼容。新项目应先检查当前 Waline 的 peerDependencies;如果项目已经升级到 Vue 3.5,可以使用更新版本并重新验证。
评论组件的核心实现如下:
<template>
<div ref="containerRef" />
</template>
<script lang="ts" setup>
import type { WalineInstance } from '@waline/client'
import { init } from '@waline/client'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import '@waline/client/waline.css'
import { useCurrentUrl } from '@/theme/utils/router'
const DEFAULT_SERVER_URL = 'https://your-waline-domain.example.com'
const containerRef = ref<HTMLElement>()
const { currentUrl } = useCurrentUrl()
let walineInstance: WalineInstance | undefined
function normalizeCommentPath(url: string) {
const pathname = new URL(url, window.location.origin).pathname
return pathname === '/' ? pathname : pathname.replace(/\/$/, '')
}
onMounted(() => {
if (!containerRef.value)
return
walineInstance = init({
el: containerRef.value,
serverURL: import.meta.env.VITE_WALINE_SERVER_URL || DEFAULT_SERVER_URL,
path: normalizeCommentPath(window.location.href),
lang: 'zh-CN',
dark: 'html.dark',
login: 'disable',
imageUploader: false,
search: false,
locale: {
placeholder: '说点什么吧...',
},
})
})
watch(currentUrl, async (url) => {
if (!walineInstance || !url)
return
await nextTick()
walineInstance.update({
path: normalizeCommentPath(url),
})
})
onBeforeUnmount(() => {
walineInstance?.destroy()
walineInstance = undefined
})
</script>用环境变量覆盖服务端地址
组件保留一个默认地址,同时允许不同环境通过 Vite 环境变量覆盖:
VITE_WALINE_SERVER_URL=https://your-waline-domain.example.comVITE_ 开头的变量会被打包到客户端,因此只能放公开的服务地址,不能放数据库连接串、管理密钥或其他凭据。
处理 VitePress 的 SPA 路由
VitePress 页面间跳转通常不会重新加载整个文档。如果只在 onMounted 中读取一次 window.location.pathname,从文章 A 跳到文章 B 后,评论组件可能仍然查询文章 A 的路径。
因此需要监听项目现有的路由状态,在 URL 变化后调用:
walineInstance.update({
path: normalizeCommentPath(url),
})组件卸载时还要调用 destroy(),防止事件监听和组件实例残留。
统一评论路径
历史数据迁移和前端查询必须使用相同的路径规则。本次约定:
- 只保留
pathname。 - 不包含域名、查询参数和 hash。
- 根路径保留为
/。 - 其他路径移除末尾
/。
例如:
https://www.example.com/message/?from=nav#comment会转换为:
/message如果历史数据中是 /message,客户端请求使用 /message/,Waline 会将其视为两个评论页面。
经验一:pnpm 与 preserveSymlinks 会影响依赖解析
第一次启动时,Vite 在预构建 @waline/client 时出现以下错误:
Could not resolve "@waline/api"
Could not resolve "marked"
Could not resolve "marked-highlight"
Could not resolve "recaptcha-v3"
Could not resolve "autosize"这些包是 Waline 客户端的依赖。项目使用 pnpm 的隔离式 node_modules,同时 Vite 配置中设置了:
resolve: {
preserveSymlinks: true,
}启用该选项后,模块解析保留顶层符号链接路径,依赖查找没有回到 pnpm 实际存储 Waline 包的位置,导致 Waline 的传递依赖无法解析。
本项目没有依赖该选项的其他场景,因此移除 preserveSymlinks,保留正常的 alias 配置:
resolve: {
alias: {
'@': path.resolve(__dirname, '../'),
},
}排查这类问题时,可以先确认报错包是否已经存在于 lockfile 和 pnpm store。包已经安装但构建器仍然找不到时,应检查符号链接解析、workspace 边界和构建器的依赖预构建配置。
经验二:先确认 Waline 与 Vue 的版本兼容性
解决模块解析后,启动又出现:
No matching export in "vue.runtime.esm-bundler.js"
for import "useTemplateRef"当时项目实际安装的是 Vue 3.4.27,而 @waline/client@3.15.2 的构建产物使用了 useTemplateRef。该 API 在当前 Vue 版本中不存在。
处理方式有两种:
- 升级 Vue 及相关 VitePress 依赖。
- 选择与现有 Vue 版本兼容的 Waline 客户端。
本次迁移希望控制影响范围,因此将 Waline 客户端调整到 3.2.7。修改版本后需要确认 lockfile 中的实际解析版本,不能只查看 package.json 中的范围。
对存量项目增加 UI SDK 时,建议先核对:
- 包的
peerDependencies。 - 发布版本实际构建产物使用的框架 API。
- lockfile 中最终解析出的版本。
- 项目当前 Vue、Vite 和 VitePress 的组合。
第五步:分析 LeanCloud 导出数据
LeanCloud 导出得到一个 CSV 文件。下面是一组实际迁移数据,用于展示迁移前需要掌握的信息:
| 项目 | 数量 |
|---|---|
| 评论总数 | 191 |
| 根评论 | 149 |
| 回复 | 42 |
| 评论页面 | 75 |
| 时间范围 | 2019-03-01 至 2026-05-29 |
| 空邮箱 | 135 |
| 空 IP | 36 |
| Base64 图片评论 | 2 |
迁移前还检查了以下内容:
objectId是否重复。- 回复引用的
pid、rid是否存在。 - 评论页面路径是否包含域名、查询参数、hash 或末尾
/。 - 评论正文是否包含体积较大的 Base64 图片。
- 邮箱、IP、User-Agent 等字段的缺失情况。
这一步决定字段映射和校验规则。不能只根据几行样例设计转换脚本。
第六步:设计字段映射
LeanCloud 的 Valine 数据使用字符串 objectId 标识评论关系,Waline PostgreSQL 表使用整数主键。迁移时需要建立稳定的 ID 映射。
本次按 insertedAt + objectId 排序,为每条源评论分配确定的整数 ID,然后转换 pid 和 rid。相同的源文件多次生成 SQL 时,映射结果保持一致。
主要字段映射如下:
| LeanCloud/Valine 字段 | Waline 字段 | 处理方式 |
|---|---|---|
objectId | id | 转换为确定的整数 ID |
pid | pid | 通过 ID 映射转换 |
rid | rid | 通过 ID 映射转换 |
url | url | 按前端规则规范化路径 |
comment | comment | 保留原始正文 |
nick | nick | 保留 |
mail | mail | 保留,允许为空 |
link | link | 保留 |
ip | ip | 保留,允许为空 |
ua | ua | 保留 |
insertedAt | insertedAt | 保留原始时间 |
updatedAt | updatedAt | 保留原始时间 |
无实际数据的头像辅助字段没有导入。历史评论没有对应的 Waline 用户记录,因此 user_id 保持为空。
第七步:生成并执行事务 SQL
转换脚本读取 CSV 后生成 PostgreSQL SQL 文件。SQL 的整体结构为:
BEGIN;
INSERT INTO "wl_Comment" (
"id",
"user_id",
"comment",
"insertedAt",
"updatedAt",
"ip",
"link",
"mail",
"nick",
"pid",
"rid",
"sticky",
"status",
"ua",
"url"
) VALUES
(...),
(...);
SELECT setval(
pg_get_serial_sequence('"wl_Comment"', 'id'),
(SELECT MAX("id") FROM "wl_Comment"),
true
);
-- 数量、关系和重复数据校验
COMMIT;导入前先检查目标表是否为空,并创建 PostgreSQL 备份。随后使用单个事务导入全部评论,在提交前执行数量、父子关系和重复数据校验。
SQL 行必须带括号
转换脚本的第一个版本生成了缺少行括号的 VALUES 数据:
VALUES
1, NULL, 'comment', ...;PostgreSQL 要求每一行数据使用括号:
VALUES
(1, NULL, 'comment', ...);第一次执行在首行报语法错误。由于所有操作都在事务中,数据库完整回滚,没有留下部分导入的数据。修正生成器后重新生成 SQL 并执行。
这个问题说明迁移脚本至少需要具备以下保护:
- 目标库导入前备份。
- 全量写入放在一个事务中。
- SQL 中包含导入后的断言。
- 失败后检查目标表,而不是直接重试。
- 生成文件记录哈希,确保执行的是已经审查过的版本。
第八步:校验导入结果
导入完成后的数据库统计为:
| 校验项 | 结果 |
|---|---|
| 评论总数 | 191 |
| 根评论 | 149 |
| 回复 | 42 |
| 评论页面 | 75 |
| 缺失父子关系 | 0 |
| 重复数据组 | 0 |
| 空邮箱 | 135 |
| 空 IP | 36 |
| 空 User-Agent | 0 |
| 最大评论 ID | 191 |
| 序列当前值 | 191 |
除聚合统计外,还将目标表重新导出,与源 CSV 按 ID 映射逐条比较 15 个字段,最终不一致数量为 0。
页面层还需要抽查:
- 一条只有根评论的文章。
- 一条包含多级回复的文章。
- 留言板等固定页面。
- 包含链接、代码或图片的评论。
- 从一个文章通过 SPA 导航切换到另一个文章。
- 新增一条评论,确认自增 ID 和历史数据没有冲突。
经验三:接口返回 302 时检查 Deployment Protection
前端接入完成后,请求评论接口一度返回:
GET /api/comment?path=%2Fmessage...
302 Found响应跳转到了 Vercel 的 SSO 页面。此时请求已经到达 Vercel,但部署启用了 Deployment Protection,浏览器无法把它作为公开评论 API 使用。
处理步骤:
- 在 Vercel 项目的 Deployment Protection 中确认当前保护策略。
- 对公开评论服务关闭不需要的访问保护。
- 在 Domains 中配置并使用稳定的 Production Domain。
- 更新
VITE_WALINE_SERVER_URL后重新构建博客。 - 在未登录 Vercel 的浏览器环境中重新请求评论 API。
带部署哈希的 URL 适合预览和排查,不适合作为长期公开 API 地址。生产环境应使用项目的 Production Domain 或自定义域名。
经验四:分层判断网络、平台和应用问题
排查过程中,Waline 部署域名在本地网络中还出现过异常 DNS 解析和连接超时,而通过本地代理可以访问 Vercel 并得到 302。
可以按以下顺序区分问题:
# 检查 DNS
dig your-waline-domain.example.com
# 检查响应头
curl -I https://your-waline-domain.example.com
# 如本机使用代理,再通过代理验证
curl -I -x http://127.0.0.1:7897 \
https://your-waline-domain.example.com- 域名无法解析或连接超时:先检查 DNS、网络和代理。
- 返回 Vercel SSO 302:检查 Deployment Protection。
- 返回 Waline JSON 错误:继续检查 Waline 配置和数据库连接。
- 返回 200 但页面没有评论:检查
path和前端实例更新。
经验五:不要把数据库连接串放进进程参数
迁移时如果直接执行:
psql 'postgresql://user:password@host/database'完整连接串可能出现在进程列表、终端历史、CI 日志或诊断输出中。即使只运行几秒,也应按凭据已暴露处理。
更合适的方式是解析连接信息后,通过 PostgreSQL 环境变量、.pgpass 或 service file 传递:
PGHOST=your-host \
PGPORT=5432 \
PGDATABASE=your-database \
PGUSER=your-user \
PGPASSWORD=your-password \
psql环境变量仍然需要控制脚本输出和运行环境权限,但可以避免连接串直接出现在命令行参数中。
如果凭据已经进入进程输出或日志,应立即:
- 在 Neon 中轮换数据库角色密码。
- 更新 Vercel 环境变量。
- 重新部署 Waline。
- 删除包含旧凭据的临时文件和日志。
- 确认旧连接串已经失效。
文章、迁移报告和提交记录中也不应包含数据库连接串、评论者邮箱、IP 或完整评论正文。
上线前检查清单
前端
- [ ] 使用 Waline 官方客户端初始化评论组件。
- [ ] 引入 Waline 样式。
- [ ] VitePress 路由变化后更新评论路径。
- [ ] 组件卸载时销毁 Waline 实例。
- [ ] 统一移除查询参数、hash 和路径末尾
/。 - [ ] 服务端地址支持环境变量覆盖。
依赖
- [ ] 检查 pnpm 实际解析版本。
- [ ] 检查
preserveSymlinks是否影响传递依赖解析。 - [ ] 选择与当前 Vue 版本兼容的 Waline 客户端。
数据
- [ ] 统计源数据总量、页面数和回复关系。
- [ ] 验证所有
pid、rid引用。 - [ ] 建立确定的字符串 ID 到整数 ID 映射。
- [ ] 导入前备份目标数据库。
- [ ] 使用事务执行全量导入。
- [ ] 同步 PostgreSQL 自增序列。
- [ ] 对关键字段执行逐条完整性比较。
部署与安全
- [ ] 使用公开的 Production Domain。
- [ ] 完成 Waline 管理员注册。
- [ ] 检查 Vercel Deployment Protection。
- [ ] 区分 DNS、代理、Vercel 和 Waline 应用错误。
- [ ] 避免在命令行参数中传递数据库连接串。
- [ ] 对可能暴露的数据库凭据执行轮换。
总结
按照本文步骤,可以先搭建一套可独立验证的 Waline 服务端,再接入博客,最后处理历史评论。每个阶段都有明确的检查点,出现问题时可以根据请求是否到达 Vercel、Waline 是否连接数据库、客户端 path 是否一致逐层定位。
实践中需要重点关注四件事:VitePress 的 SPA 路由会改变评论页面路径;Waline 客户端版本需要与 Vue 版本匹配;LeanCloud 字符串 ID 需要稳定转换并保留回复关系;Vercel 的预览地址和访问保护不一定适合作为公开 API。
本文案例最终迁移了 191 条历史评论、42 条回复和 75 个页面路径,关系校验与逐字段比较均无差异。这些统计不是固定目标,实际实施时应从自己的源数据生成对应基线。
参考资料
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
