VitePress博客从Valine迁移到Waline:部署、接入与数据迁移

发布于 | 分类于 博客|本文包含AIGC内容

很久没看博客的留言了,最近登录一下,发现之前用的leancloud已经要停服了

决定来迁移一下,把相关的数据迁移到一个新的留言系统,经过调研,最终决定迁移到Valine。

本文涉及Waline 客户端、Vercel 服务端和 Neon PostgreSQL。内容从账号和项目创建开始,依次完成数据库初始化、VitePress 接入、LeanCloud 历史数据转换、导入校验和上线检查。即使没有历史数据,也可以只使用前半部分完成一个新的 Waline 评论系统。

完成后会得到什么

最终调用关系如下:

text
VitePress 博客

    │ @waline/client

Vercel 上的 Waline 服务端

    │ PostgreSQL 环境变量

Neon PostgreSQL

这个组合不会改变静态博客的部署方式。博客页面只加载评论组件,评论查询、提交和管理由 Waline 服务端处理,数据存储在独立的 PostgreSQL 数据库中。

完整实施分为四个阶段:

  1. 在 Vercel 创建 Waline 服务端项目。
  2. 创建 Neon PostgreSQL 数据库并初始化 Waline 表结构。
  3. 在 VitePress 中接入 @waline/client
  4. 将 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 仓库和项目:

  1. 使用 GitHub 登录 Vercel。
  2. 输入项目名,例如 my-blog-waline
  3. 选择自己的 GitHub 账号或组织作为仓库归属。
  4. 点击 Create,等待首次部署完成。
  5. 部署完成后点击 Go to Dashboard 进入项目控制台。

首次部署只代表 Waline 服务端代码已经运行。数据库创建和表结构初始化完成后,评论 API 才能正常读写数据。

Vercel 每次部署都会生成一个唯一的 Deployment URL。后续应在 Settings → Domains 中确认项目的 Production Domain,并让博客客户端使用这个稳定地址。带有部署哈希和账号名的预览地址可能受到 Deployment Protection 限制。

第二步:创建 Neon PostgreSQL 数据库

方式一:从 Vercel Storage 创建

这是 Waline 官方部署指南当前提供的默认流程:

  1. 在 Waline 项目控制台进入 Storage
  2. 点击 Create Database
  3. Marketplace Database Providers 中选择 Neon
  4. 接受 Neon 授权,选择计划、区域和配额。
  5. 输入数据库名称并完成创建。
  6. 确认数据库已经连接到当前 Waline 项目。

Vercel Marketplace 会把数据库连接变量添加到项目中。变量名称和数量可能随集成版本变化,可以在 Settings → Environment Variables 中确认,不要把这些变量复制到博客前端。

接着点击数据库的 Open in Neon,在 Neon 控制台中进入 SQL Editor。打开 Waline 仓库中的 PostgreSQL 初始化脚本 waline.pgsql,复制完整 SQL 并执行,用于创建评论、用户和计数等表。

执行成功后回到 Vercel:

  1. 进入 Deployments
  2. 对最新的生产部署执行 Redeploy
  3. 等待状态变成 Ready
  4. 访问 Production Domain,确认可以打开 Waline 服务端页面。

Vercel 环境变量只会在新部署中生效,因此数据库创建后必须重新部署。

方式二:连接已经创建的 Neon 数据库

如果已经在 Neon 独立创建了项目,可以参考 Neon 官方的 Vercel 手动连接指南 获取连接信息:

  1. 在 Neon 项目首页点击 Connect
  2. 选择需要使用的 branch、database 和 role。
  3. 复制 PostgreSQL connection string。
  4. 打开 Vercel 项目的 Settings → Environment Variables
  5. 将连接串中的信息拆分成 Waline 支持的 PostgreSQL 环境变量。
  6. 根据实际需要勾选 Production、Preview 和 Development 环境。
  7. 保存变量并重新部署 Waline。

Waline 官方文档列出的变量如下:

text
PG_DB=数据库名
PG_USER=角色名
PG_PASSWORD=角色密码
PG_HOST=Neon主机名
PG_PORT=5432
PG_SSL=true

也可以使用对应的 POSTGRES_DATABASEPOSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_HOSTPOSTGRES_PORTPOSTGRES_SSL 别名。以 Waline 服务端环境变量文档 为准。

Neon 的通用指南使用 DATABASE_URL 连接普通 Vercel 应用,但 Waline 当前文档没有把 DATABASE_URL 列为 PostgreSQL 配置项。手动配置时不要只添加一个通用连接串。

然后仍需在 Neon SQL Editor 中执行 Waline 的 waline.pgsql。连接串属于服务端凭据,只能保存在受控的环境变量或本地密钥文件中。

第三步:验证服务端并注册管理员

先访问:

text
https://你的-production-domain/

页面能够打开后,再访问:

text
https://你的-production-domain/ui/register

第一个注册的用户会成为 Waline 管理员,可以在 /ui 中管理评论。建议在正式开放评论前完成管理员注册。

还可以直接请求一个空页面的评论接口:

text
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

bash
pnpm add @waline/client@3.2.7

这里使用确定版本是为了与 Vue 3.4 保持兼容。新项目应先检查当前 Waline 的 peerDependencies;如果项目已经升级到 Vue 3.5,可以使用更新版本并重新验证。

评论组件的核心实现如下:

vue
<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 环境变量覆盖:

bash
VITE_WALINE_SERVER_URL=https://your-waline-domain.example.com

VITE_ 开头的变量会被打包到客户端,因此只能放公开的服务地址,不能放数据库连接串、管理密钥或其他凭据。

处理 VitePress 的 SPA 路由

VitePress 页面间跳转通常不会重新加载整个文档。如果只在 onMounted 中读取一次 window.location.pathname,从文章 A 跳到文章 B 后,评论组件可能仍然查询文章 A 的路径。

因此需要监听项目现有的路由状态,在 URL 变化后调用:

ts
walineInstance.update({
  path: normalizeCommentPath(url),
})

组件卸载时还要调用 destroy(),防止事件监听和组件实例残留。

统一评论路径

历史数据迁移和前端查询必须使用相同的路径规则。本次约定:

  • 只保留 pathname
  • 不包含域名、查询参数和 hash。
  • 根路径保留为 /
  • 其他路径移除末尾 /

例如:

text
https://www.example.com/message/?from=nav#comment

会转换为:

text
/message

如果历史数据中是 /message,客户端请求使用 /message/,Waline 会将其视为两个评论页面。

第一次启动时,Vite 在预构建 @waline/client 时出现以下错误:

text
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 配置中设置了:

ts
resolve: {
  preserveSymlinks: true,
}

启用该选项后,模块解析保留顶层符号链接路径,依赖查找没有回到 pnpm 实际存储 Waline 包的位置,导致 Waline 的传递依赖无法解析。

本项目没有依赖该选项的其他场景,因此移除 preserveSymlinks,保留正常的 alias 配置:

ts
resolve: {
  alias: {
    '@': path.resolve(__dirname, '../'),
  },
}

排查这类问题时,可以先确认报错包是否已经存在于 lockfile 和 pnpm store。包已经安装但构建器仍然找不到时,应检查符号链接解析、workspace 边界和构建器的依赖预构建配置。

经验二:先确认 Waline 与 Vue 的版本兼容性

解决模块解析后,启动又出现:

text
No matching export in "vue.runtime.esm-bundler.js"
for import "useTemplateRef"

当时项目实际安装的是 Vue 3.4.27,而 @waline/client@3.15.2 的构建产物使用了 useTemplateRef。该 API 在当前 Vue 版本中不存在。

处理方式有两种:

  1. 升级 Vue 及相关 VitePress 依赖。
  2. 选择与现有 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
空 IP36
Base64 图片评论2

迁移前还检查了以下内容:

  • objectId 是否重复。
  • 回复引用的 pidrid 是否存在。
  • 评论页面路径是否包含域名、查询参数、hash 或末尾 /
  • 评论正文是否包含体积较大的 Base64 图片。
  • 邮箱、IP、User-Agent 等字段的缺失情况。

这一步决定字段映射和校验规则。不能只根据几行样例设计转换脚本。

第六步:设计字段映射

LeanCloud 的 Valine 数据使用字符串 objectId 标识评论关系,Waline PostgreSQL 表使用整数主键。迁移时需要建立稳定的 ID 映射。

本次按 insertedAt + objectId 排序,为每条源评论分配确定的整数 ID,然后转换 pidrid。相同的源文件多次生成 SQL 时,映射结果保持一致。

主要字段映射如下:

LeanCloud/Valine 字段Waline 字段处理方式
objectIdid转换为确定的整数 ID
pidpid通过 ID 映射转换
ridrid通过 ID 映射转换
urlurl按前端规则规范化路径
commentcomment保留原始正文
nicknick保留
mailmail保留,允许为空
linklink保留
ipip保留,允许为空
uaua保留
insertedAtinsertedAt保留原始时间
updatedAtupdatedAt保留原始时间

无实际数据的头像辅助字段没有导入。历史评论没有对应的 Waline 用户记录,因此 user_id 保持为空。

第七步:生成并执行事务 SQL

转换脚本读取 CSV 后生成 PostgreSQL SQL 文件。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 数据:

sql
VALUES
  1, NULL, 'comment', ...;

PostgreSQL 要求每一行数据使用括号:

sql
VALUES
  (1, NULL, 'comment', ...);

第一次执行在首行报语法错误。由于所有操作都在事务中,数据库完整回滚,没有留下部分导入的数据。修正生成器后重新生成 SQL 并执行。

这个问题说明迁移脚本至少需要具备以下保护:

  • 目标库导入前备份。
  • 全量写入放在一个事务中。
  • SQL 中包含导入后的断言。
  • 失败后检查目标表,而不是直接重试。
  • 生成文件记录哈希,确保执行的是已经审查过的版本。

第八步:校验导入结果

导入完成后的数据库统计为:

校验项结果
评论总数191
根评论149
回复42
评论页面75
缺失父子关系0
重复数据组0
空邮箱135
空 IP36
空 User-Agent0
最大评论 ID191
序列当前值191

除聚合统计外,还将目标表重新导出,与源 CSV 按 ID 映射逐条比较 15 个字段,最终不一致数量为 0。

页面层还需要抽查:

  • 一条只有根评论的文章。
  • 一条包含多级回复的文章。
  • 留言板等固定页面。
  • 包含链接、代码或图片的评论。
  • 从一个文章通过 SPA 导航切换到另一个文章。
  • 新增一条评论,确认自增 ID 和历史数据没有冲突。

经验三:接口返回 302 时检查 Deployment Protection

前端接入完成后,请求评论接口一度返回:

text
GET /api/comment?path=%2Fmessage...
302 Found

响应跳转到了 Vercel 的 SSO 页面。此时请求已经到达 Vercel,但部署启用了 Deployment Protection,浏览器无法把它作为公开评论 API 使用。

处理步骤:

  1. 在 Vercel 项目的 Deployment Protection 中确认当前保护策略。
  2. 对公开评论服务关闭不需要的访问保护。
  3. 在 Domains 中配置并使用稳定的 Production Domain。
  4. 更新 VITE_WALINE_SERVER_URL 后重新构建博客。
  5. 在未登录 Vercel 的浏览器环境中重新请求评论 API。

带部署哈希的 URL 适合预览和排查,不适合作为长期公开 API 地址。生产环境应使用项目的 Production Domain 或自定义域名。

经验四:分层判断网络、平台和应用问题

排查过程中,Waline 部署域名在本地网络中还出现过异常 DNS 解析和连接超时,而通过本地代理可以访问 Vercel 并得到 302。

可以按以下顺序区分问题:

bash
# 检查 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 和前端实例更新。

经验五:不要把数据库连接串放进进程参数

迁移时如果直接执行:

bash
psql 'postgresql://user:password@host/database'

完整连接串可能出现在进程列表、终端历史、CI 日志或诊断输出中。即使只运行几秒,也应按凭据已暴露处理。

更合适的方式是解析连接信息后,通过 PostgreSQL 环境变量、.pgpass 或 service file 传递:

bash
PGHOST=your-host \
PGPORT=5432 \
PGDATABASE=your-database \
PGUSER=your-user \
PGPASSWORD=your-password \
psql

环境变量仍然需要控制脚本输出和运行环境权限,但可以避免连接串直接出现在命令行参数中。

如果凭据已经进入进程输出或日志,应立即:

  1. 在 Neon 中轮换数据库角色密码。
  2. 更新 Vercel 环境变量。
  3. 重新部署 Waline。
  4. 删除包含旧凭据的临时文件和日志。
  5. 确认旧连接串已经失效。

文章、迁移报告和提交记录中也不应包含数据库连接串、评论者邮箱、IP 或完整评论正文。

上线前检查清单

前端

  • [ ] 使用 Waline 官方客户端初始化评论组件。
  • [ ] 引入 Waline 样式。
  • [ ] VitePress 路由变化后更新评论路径。
  • [ ] 组件卸载时销毁 Waline 实例。
  • [ ] 统一移除查询参数、hash 和路径末尾 /
  • [ ] 服务端地址支持环境变量覆盖。

依赖

  • [ ] 检查 pnpm 实际解析版本。
  • [ ] 检查 preserveSymlinks 是否影响传递依赖解析。
  • [ ] 选择与当前 Vue 版本兼容的 Waline 客户端。

数据

  • [ ] 统计源数据总量、页面数和回复关系。
  • [ ] 验证所有 pidrid 引用。
  • [ ] 建立确定的字符串 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 个页面路径,关系校验与逐字段比较均无差异。这些统计不是固定目标,实际实施时应从自己的源数据生成对应基线。

参考资料

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。