在 Vercel 上开发和部署订阅功能的完整指南

——从零搭建到成功联调的完整踩坑记录

背景

项目 chuhai168.top 是一个部署在 Vercel 的静态网站,需要实现一个邮件订阅功能:

  • 用户提交邮箱订阅 → 发邮件确认 → 点击确认链接完成订阅。
  • 后端服务托管在 Vercel,使用 Neon Serverless Postgres 存储订阅记录,Resend 作为邮件发送服务。
  • 前端和后端分离,前端纯静态 HTML/JS,后端作为单独 Vercel 项目 api.chuhai168.top 提供 API。

在搭建过程中,遇到了多种典型问题:TypeScript 版本冲突、CORS 配置错误、DNS 解析失效、TLS 握手失败等。这篇文章完整总结整个过程。

功能架构

[前端静态站 chuhai168.top]
      |
      v
 [API Gateway: api.chuhai168.top]
      |
      v
[Neon Serverless Postgres]  <-- 存储用户邮箱、订阅状态
[Resend 邮件服务]           <-- 发送确认/欢迎邮件
  • 前端使用 fetch('/api/subscribe') 调用后端 API。
  • 后端 Edge Function 提供 /api/subscribe/api/confirm 两个接口。
  • Neon 数据库存储用户订阅状态:pending → confirmed。
  • Resend 邮件服务负责发邮件,域名配置了 DKIM/SPF 保障投递。

主要开发步骤

1初始化 API 项目

  • 使用 Vercel 创建新项目 chuhai168-api
  • 项目结构:
api/
  subscribe.ts   # 接收邮箱订阅请求,写数据库,发送确认邮件
  confirm.ts     # 用户点击邮件确认链接,修改状态为 confirmed
package.json
tsconfig.json

安装依赖:

pnpm add @neondatabase/serverless
pnpm add -D typescript

2数据库与表结构

使用 Neon Serverless Postgres:

CREATE TABLE IF NOT EXISTS subscribers (
  id SERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  status TEXT CHECK (status IN ('pending','confirmed')),
  token TEXT UNIQUE,
  created_at TIMESTAMP DEFAULT now()
);

3邮件服务配置

  • 注册 Resend,获取 API Key。
  • 添加自定义域 chuhai168.top,配置:
    • DKIM CNAME
    • SPF TXT
    • DMARC TXT
  • 验证成功后 FROM_EMAIL 使用 no-reply@chuhai168.top

4后端接口逻辑

简化版逻辑:

// subscribe.ts
const rows = (await sql`
  SELECT status FROM subscribers WHERE email = ${email} LIMIT 1
`) as any[];

if (!rows.length) {
  // 插入记录
  const token = crypto.randomUUID();
  await sql`
    INSERT INTO subscribers (email, status, token)
    VALUES (${email}, 'pending', ${token})
  `;
  // 发送确认邮件
  await resend.emails.send({
    from: FROM_EMAIL,
    to: email,
    subject: "请确认订阅",
    html: `<a href="${CONFIRM_URL}?token=${token}">点击确认</a>`
  });
}

踩坑与解决方案

🔹 1. TypeScript 编译报错

错误:

TS2344: Type '{ status: string; }[]' does not satisfy the constraint 'boolean'.

原因:TS 4.9 不兼容 Neon 模板标签的泛型。

解决方案:

  • 升级 TS 至 5.4;
  • 或移除泛型,改用 as any[] 明确类型断言。

🔹 2. Vercel 未使用最新 package.json

问题:Vercel 构建日志仍显示 Using built-in TypeScript 4.9.5,没读到 devDependencies。

原因:构建目录或缓存问题。

解决方案:

  • 确认 Root Directory 设置正确;
  • Redeploy 时选择 Clear build cache
  • 或直接用 npx vercel --prod 本地上传部署。

🔹 3. CORS 报错

错误:

No 'Access-Control-Allow-Origin' header is present

解决方案:

  • 在 API 项目设置 ALLOWED_ORIGINS=https://chuhai168.top,https://www.chuhai168.top
  • 后端在响应头中动态注入:
res.headers.set('Access-Control-Allow-Origin', origin);

🔹 4. 域名解析错误

问题:

  • dig api.chuhai168.top 显示 198.18.0.119,不是 Vercel;
  • TLS 握手失败:LibreSSL SSL_connect: SSL_ERROR_SYSCALL

原因:api 子域是 A 记录,指向错误 IP。

解决方案:

  • 删除 api 的 A 记录;
  • 添加 api 的 CNAME:
api -> cname.vercel-dns.com
  • 等待生效并重新签发证书。

🔹 5. 最终联调

修正 DNS 后:

curl -i -X POST https://api.chuhai168.top/api/subscribe \
  -H "Origin: https://www.chuhai168.top" \
  -H "Content-Type: application/json" \
  --data '{"email":"test@example.com","hp":""}'

返回:

{"ok":true,"message":"订阅请求已发送,请前往邮箱点击确认链接完成订阅。"}

浏览器端提交表单成功,邮件送达,点击确认链接跳转首页提示"订阅成功"。

总结

在 Vercel 上开发和部署完整订阅功能的流程:

  1. 分离前后端项目:前端纯静态部署,后端用 Edge Function 提供 API。
  2. 邮件服务:推荐 Resend,域名配置 DKIM/SPF 确保投递。
  3. 数据库:Neon Serverless Postgres,轻量且无服务器化。
  4. CORS 配置:根据前端实际域名动态允许跨域。
  5. DNS 正确配置:子域必须 CNAME 到 cname.vercel-dns.com,否则证书无法签发。
  6. 调试工具
    • curldig 是排查 API 和 DNS 的必备工具;
    • openssl s_client 查看 TLS 细节;
    • Vercel CLI 可快速从本地强制部署。
  7. 最佳实践
    • 所有敏感变量放在 Vercel 环境变量;
    • API 响应必须显式设置 Access-Control-Allow-*
    • 发布前先验证默认 *.vercel.app 域是否正常,再配置自定义域。
← 返回首页