跳到主要内容

从 Vercel 迁移

将 Vercel 部署的项目(静态站点、Next.js、Serverless Functions 等)迁移至腾讯云开发 CloudBase

迁移总览

Vercel 和 CloudBase 在核心能力上高度对齐,都提供前端 + Serverless 后端 + 数据存储的一站式部署体验。迁移主要围绕 静态托管后端计算(云函数 & 云托管)数据库文件存储身份认证环境变量 六个模块逐项适配。

对比维度VercelCloudBase
静态托管自动从 Git 构建部署静态网站托管 + CDN 加速
Serverless 函数Vercel Functions(Node/Go/Py/Ruby)云函数(Node.js / PHP / Python / Golang / Java)+ 云托管(任意语言/框架)
边缘函数Edge Functions / Middleware云托管函数框架(支持 SSE、WebSocket)
数据库Vercel Marketplace(Neon Postgres / Supabase 等第三方)文档型数据库(NoSQL)+ MySQL 数据库 + PostgreSQL 数据库
对象存储Vercel Blob Storage云存储(内置 CDN + 图片处理)
身份认证第三方集成(Clerk / Auth0 / NextAuth)内置身份认证(微信/短信/邮箱/匿名/自定义)
自定义域名Dashboard 一键绑定控制台绑定 + 用户自行上传 SSL 证书
环境变量Dashboard / CLI 管理控制台 / CLI 管理

快速对照速查表

我现在的 Vercel 用法迁移到 CloudBase 的对应方式
部署纯静态站点(Vite/React/Vue)tcb app deploy -e <envId>
部署 Next.js SSR云托管 + Dockerfile
部署 Next.js 静态导出同纯静态站点
api/*.ts Serverless Functions云函数 或 云托管 Express 应用
middleware.ts 边缘逻辑云托管中间件 / API 网关
环境变量 process.env.XXX控制台配置,代码无需改动
自定义域名控制台「域名管理」绑定
Vercel Postgres(Marketplace · Neon)CloudBase PostgreSQL 数据库 / MySQL 数据库
Vercel Blob StorageCloudBase 云存储

模块一:静态托管

CloudBase 静态网站托管:内置 CDN、HTTPS、SPA Fallback、HTTP 响应头、路由规则。控制台在「静态网站托管」。

1. 纯静态站点(Vite / Vue / React / Svelte / Astro / Hugo)

适用:构建产物是纯 HTML/CSS/JS,不依赖 Vercel Functions

# 安装 CLI
npm install -g @cloudbase/cli

# 登录
tcb login

# 一键构建 + 部署(自动检测框架,执行 install → build → 上传 → 路由绑定)
tcb app deploy -e <envId>

# 或指定框架类型
tcb app deploy --framework vite -e <envId>

# 部署到指定子路径 /app
tcb app deploy --deploy-path /app -e <envId>

一行命令替代 Vercel 的 Git 自动部署。tcb hosting deploy 仅适用于上传已构建好的纯静态文件,不是标准应用部署路径。

2. 静态框架部署(Next.js / Nuxt.js / SvelteKit / Astro)

如果使用框架的静态导出模式(构建产物是纯 HTML/CSS/JS):

框架静态导出配置部署命令
Next.jsnext.config.js 中设置 output: 'export'tcb app deploy --framework next --output-dir out -e <envId>
Nuxt.jsnuxt.config.ts 中设置 ssr: falsetcb app deploy -e <envId>
SvelteKitsvelte.config.js 中配置 @sveltejs/adapter-statictcb app deploy -e <envId>
Astro默认即为静态(output: 'static'tcb app deploy -e <envId>

tcb app deploy 会自动检测框架类型并执行 install → build → 上传 → 路由绑定。Next.js 因输出目录需显式指定,其他框架会自动识别。


模块二:后端计算(云函数 & 云托管)

Vercel Functions 的替代方案分两个形态:轻量独立 API 用云函数,多路由 / SSR 框架场景用云托管。按复杂度选型即可。

场景速览

Vercel 用法CloudBase 方案适合什么情况
api/ 下少量独立文件云函数API 不多、每个函数独立部署,改动最小
api/ 下 5+ 文件或需路由管理云托管(Express/Koa)避免函数碎片化,统一路由和中间件
Next.js / Nuxt / SvelteKit / Astro云托管(Docker)框架自带 SSR 和服务端路由,需容器环境

1. 场景一:纯 API 路由 → 云函数

CloudBase 云函数:Node.js / PHP / Python / Golang / Java 运行时、HTTP/定时/数据库触发、最长 900s 超时。控制台在「云函数」。

能力对照

对比维度Vercel FunctionsCloudBase 云函数
运行环境Node.js / Go / Python / RubyNode.js / PHP / Python / Golang / Java
触发方式HTTP 路径自动路由HTTP 触发 / 定时触发 / 数据库触发
路径映射api/*.ts/api/*每个函数一个路径
超时时间10s(Hobby)/ 60s(Pro)最长 900s
内存128MB - 3008MB256MB - 3072MB
冷启动较快(边缘部署)可配置保活(minNum >= 1

目录结构迁移

Vercel 项目结构:

api/
hello.ts → /api/hello
users/[id].ts → /api/users/:id
webhook.ts → /api/webhook

CloudBase 云函数结构:

cloudfunctions/
hello/
index.js
package.json
users/
index.js
package.json

提示api/ 下文件数量较多但暂不合并到云托管时,可使用文末「迁移脚本工具」中的 batch-migrate-functions.js 一键生成上述骨架。

代码示例

Vercel(api/hello.ts):

// api/hello.ts
import type { VercelRequest, VercelResponse } from '@vercel/node'

export default function handler(req: VercelRequest, res: VercelResponse) {
const { name = 'World' } = req.query
res.status(200).json({ message: `Hello, ${name}!` })
}

CloudBase(cloudfunctions/hello/index.js):

// cloudfunctions/hello/index.js
exports.main = async (event, context) => {
const { name = 'World' } = event.queryStringParameters || {}
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello, ${name}!` }),
}
}

提示:API 路由数量超过 5 个时,建议跳转到场景二,用云托管 Express 统一管理路由,避免云函数碎片化。

部署步骤

# Step 1:创建云函数目录
mkdir -p cloudfunctions/hello && cd cloudfunctions/hello

# Step 2:初始化 package.json(云函数必须有)
npm init -y

# Step 3:将上面的 CloudBase 版本代码写入 index.js

# Step 4:部署
tcb fn deploy hello -e <envId>

# Step 5:测试调用
tcb fn invoke hello -e <envId> --params '{"path":"/api/hello","httpMethod":"GET","queryStringParameters":{"name":"CloudBase"}}'

部署完成后在控制台「云函数 → hello」可看运行状态和日志。


2. 场景二:多 API 合并 → 云托管 Express

api/ 下有 5+ 个路由,拆成独立云函数管理成本高。直接用一个云托管服务统一承载所有 API。

// cloudrun-app/index.js(Express 示例)
const express = require('express')
const app = express()

app.get('/api/hello', (req, res) => {
res.json({ message: `Hello, ${req.query.name || 'World'}!` })
})

app.get('/api/users/:id', (req, res) => {
res.json({ id: req.params.id })
})

app.post('/api/webhook', express.json(), (req, res) => {
console.log('webhook received:', req.body)
res.json({ ok: true })
})

app.listen(3000)

部署步骤

# Step 1:准备 Dockerfile
# FROM node:20-alpine
# WORKDIR /app
# COPY package*.json ./
# RUN npm ci --only=production
# COPY . .
# EXPOSE 3000
# CMD ["node", "index.js"]

# Step 2:部署到云托管
tcb cloudrun deploy -e <envId> -s api --source ./cloudrun-app

# Step 3:访问应用
# https://<envId>-api.ap-shanghai.run.tcloudbase.com/api/hello

3. 场景三:SSR 框架 → 云托管

CloudBase 云托管:容器化部署、支持任意语言/框架、长连接(SSE/WebSocket)、无超时限制。控制台在「云托管」。

Next.js SSR / ISR / API Routes

适用:项目依赖 Next.js 服务端能力(SSR、API Routes、Middleware、ISR)

准备 Dockerfile:

# Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY .next .next
COPY public public
COPY next.config.js .

EXPOSE 3000

CMD ["node_modules/.bin/next", "start"]

构建并部署:

# 构建 Next.js 生产版本
npx next build

# 使用 CLI 部署到云托管(容器模式)
# 注:CLI 仅支持 --source 指定代码目录,CPU/内存/实例数需在控制台服务详情中配置
tcb cloudrun deploy \
-e <envId> \
-s nextjs-app \
--source .

API Routes 处理:

Vercel 上的 /api/* routes 在云托管中原样运行,无需修改代码:

// pages/api/hello.ts(无需改动)
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from CloudBase!' })
}

部署命令汇总:

整个 Next.js 应用打包为 Docker 镜像部署到云托管,API Routes、SSR、Middleware 全部自动托管:

# Step 1:构建 Next.js 生产版本
npx next build

# Step 2:准备 Dockerfile(参考上面模板)
# Step 3:部署到云托管
tcb cloudrun deploy -e <envId> -s nextjs-app --source .

# Step 4:访问应用
# 部署完成后控制台「云托管 → nextjs-app」查看访问域名

Nuxt.js / SvelteKit / Astro SSR

与 Next.js 模式相同,用 Dockerfile + 云托管部署。

# Step 1:构建生产产物(按框架选其一)
npx nuxi build # Nuxt
npm run build # SvelteKit
npm run build # Astro

# Step 2:准备 Dockerfile(按框架选对应 CMD)
# Nuxt: CMD ["node", ".output/server/index.mjs"]
# SvelteKit: CMD ["node", "build/index.js"]
# Astro: CMD ["node", "dist/server/entry.mjs"]

# Step 3:部署到云托管
tcb cloudrun deploy -e <envId> -s my-app --source .

# Step 4:避免冷启动
# 控制台「[云托管 → 服务配置](https://tcb.cloud.tencent.com/dev?envId=your-env-id#/platform-run)」将 MinNum 调到 ≥1

模块三:数据库

CloudBase 提供三大数据库:PostgreSQL、MySQL(关系型)+ 文档型数据库(NoSQL)。控制台在「数据库」。

Vercel 数据库类型CloudBase 对应说明
Marketplace · Neon(关系型 Postgres)集成PostgreSQL 数据库 / MySQL 数据库推荐优先用 PG,生态直接兼容
Marketplace · Supabase(Postgres 全套)集成PostgreSQL 数据库含 Auth/Storage/Realtime,可只搬数据库部分
Vercel Edge Config(低延迟只读配置)自营文档型数据库仅适合读写不频繁的小数据(feature flags 等)

1. 关系型数据迁移

推荐:Marketplace · Neon(Postgres)→ CloudBase PostgreSQL(同生态,直接兼容)

整体流程

本地导出 backup.sql → 上传云存储(获取 fileID) → 部署云函数 → 触发执行
    Step 1       Step 2           Step 3     Step 4

通过云函数 + executePGSql 完成导入,适合自动化 / 大文件分批。

部署步骤

Step 1:本地导出数据(本机执行,需联通 Marketplace · Neon)

pg_dump --no-owner --no-acl --inserts postgres://<vercel-conn-str> > backup.sql

Step 2:上传到云存储

控制台「云存储 → 上传」→ 获取 fileID(如 cloud://env-xxx/backup.sql)。

Step 3:部署云函数并配置环境变量

云函数需配置 TENCENTCLOUD_SECRETID / TENCENTCLOUD_SECRETKEY(控制台「云函数 → 函数配置」→ 环境变量)。

mkdir -p cloudfunctions/pg-migrate && cd cloudfunctions/pg-migrate
npm init -y
npm i @cloudbase/manager-node
# 把下方"云函数实现"代码写入 index.js

tcb fn deploy pg-migrate -e <envId>

Step 4:触发执行(注意 params 传 fileID,不是 SQL 内容)

tcb fn invoke pg-migrate -e <envId> \
--params '{"envId":"<envId>","fileID":"cloud://<envId>/backup.sql"}'

云函数实现

// cloudfunctions/pg-migrate/index.js
const CloudBase = require('@cloudbase/manager-node')

exports.main = async (event) => {
const { envId, fileID } = event
const app = CloudBase.init({
secretId: process.env.TENCENTCLOUD_SECRETID,
secretKey: process.env.TENCENTCLOUD_SECRETKEY,
envId
})

// 1. 从云存储下载 backup.sql
const { FileContent } = await app.storage.downloadFile({ fileID })
const sqlText = Buffer.from(FileContent).toString('utf-8')

// 2. 拆分 SQL:去掉注释行 + 按 ; 分句
// 注意:含 $$ ... $$ 函数体、COPY ... FROM stdin 不适用此拆分
const stmts = sqlText
.split(/;\s*(?:\n|$)/)
.map(s => s.replace(/^--.*$/gm, '').trim())
.filter(Boolean)

// 3. 逐条执行,失败不中断(记录到日志)
let succeeded = 0
const errors = []
for (const sql of stmts) {
try {
await app.database.executePGSql({ Sql: sql + ';' })
succeeded++
} catch (e) {
errors.push({ sql: sql.slice(0, 200), message: e.message })
}
}
return { total: stmts.length, succeeded, failed: errors.length, errors }
}

注意事项

  • executePGSql 返回 Columns + Rows + AffectedRows,支持 DDL / DML / DQL。
  • ⚠️ 限制pg_dumpCREATE FUNCTION ... $$ ... $$;COPY ... FROM stdin; 时,按 ; 拆分 SQL 的逻辑可能出错,建议改用 --inserts 模式导出纯 INSERT 语句,或在脚本中增加 $$ 块识别。

模块四:文件迁移

CloudBase 云存储:内置 CDN 域名、免费 HTTPS、图片处理(缩略图/裁剪/水印)、安全规则细粒度控制。控制台在「存储 → 云存储」。

CloudBase 承接能力:Vercel Blob 是对象存储,CloudBase 对应的是云存储——内置 CDN 域名、免费 HTTPS、图片处理(缩略图/裁剪/水印)、安全规则细粒度控制。Blob 文件本身的迁移脚本在下方给出,存量业务的「上传/读取」代码改动主要在 API 替换。

1. API 映射

Vercel Blob(@vercel/blobCloudBase 云存储(@cloudbase/js-sdk v3 / @cloudbase/manager-node说明
put(path, file)app.uploadFile({ cloudPath, filePath })路径直接对应(Vercel pathname → CloudBase cloudPath)
del(url)app.deleteFile({ fileList: [fileID] })CloudBase 接收 cloud://... 形式的 fileID
get(path)app.getTempFileURL({ fileList: [{fileID, maxAge}] })公开读权限可获取永久链接;私有读必须通过临时链接
list({ prefix })Web/Node SDK 无;Manager Node SDKstorage.listDirectoryFiles(cloudPath[, limit, marker])浏览器端需业务侧维护文件清单;Node.js 脚本或云函数中可使用 @cloudbase/manager-node 拉取;也可在控制台「存储 → 文件管理」手动导出
head(path)无原生批量 API;Manager Node SDKstorage.getFileInfo(cloudPath) 取单文件元信息批量场景建议在文档型数据库维护清单;单文件查询可使用 @cloudbase/manager-node
access: 'public' / 'private'存储权限(控制台「云存储 → 权限设置」)CloudBase 走目录级权限:公有读 / 私有读 / 多种用户身份读

2. 迁移脚本(Node.js)

适合中小体量(万级文件)。Vercel Blob 通过 list 获取全部 pathname,逐个下载再上传到 CloudBase;CloudBase SDK 未提供 listFiles,所以走「Vercel 拉清单 → CloudBase 写文件」的链路。

// install: npm i @vercel/blob @cloudbase/js-sdk
const { list } = require('@vercel/blob')
const cloudbase = require('@cloudbase/js-sdk').default
const fs = require('fs')
const path = require('path')

const app = cloudbase.init({ env: 'your-env-id' })
const TMP_DIR = path.join(__dirname, '_blob_tmp')

async function migrateVercelBlob({ prefix = '', token }) {
let cursor
let total = 0
do {
const { blobs, hasMore, cursor: next } = await list({ prefix, cursor, token })
for (const blob of blobs) {
// 1. 从 Vercel Blob 下载到本地
const localPath = path.join(TMP_DIR, blob.pathname)
fs.mkdirSync(path.dirname(localPath), { recursive: true })
const res = await fetch(blob.url) // Vercel Blob 的 url 可直接下载
const buf = Buffer.from(await res.arrayBuffer())
fs.writeFileSync(localPath, buf)

// 2. 上传到 CloudBase 云存储(cloudPath 与 Vercel pathname 保持一致)
const { fileID } = await app.uploadFile({
cloudPath: blob.pathname,
fileContent: fs.createReadStream(localPath),
})
console.log(`${blob.pathname} -> ${fileID}`)
total++
}
cursor = next
} while (cursor)
console.log(`迁移完成,共 ${total} 个文件`)
}

migrateVercelBlob({ token: process.env.BLOB_READ_WRITE_TOKEN })

大文件(GB 级)建议先在 Vercel 控制台「Blob → Download All」下载 ZIP 压缩包,再分批上传到 CloudBase 控制台「云存储 → 上传」,避免脚本长时间占用导致超时。

3. 脚本使用方式

本节说明 §2 迁移脚本的三种运行方式。vercel-blob-migrate 同时是 §2 脚本对应的目录名云函数名(二者同名),下文中任一处出现该名称均指同一份脚本。运行时按体量选择 A / B / C 中的任一方式即可。

迁移脚本需要在能访问 Vercel + CloudBase API 的环境中运行。推荐放在 CloudBase 自己的云函数 / 云托管里跑——免运维、自带日志、不占本地带宽。

方式 A:作为 CloudBase 云函数(⭐ 推荐,中小体量)

适合万级以下文件。云函数有 5 分钟超时限制,建议在脚本里增加「批次大小 + 续跑 cursor」逻辑,把大任务拆成多次调用。

# 1. 创建云函数目录
mkdir -p cloudfunctions/vercel-blob-migrate && cd cloudfunctions/vercel-blob-migrate
npm init -y
npm i @vercel/blob @cloudbase/js-sdk

# 2. 把上面脚本贴到 index.js,并改写入口:
# exports.main = async () => {
# await migrateVercelBlob({ token: process.env.BLOB_READ_WRITE_TOKEN })
# }

# 3. 在控制台「云函数 → vercel-blob-migrate → 函数配置」添加环境变量:
# BLOB_READ_WRITE_TOKEN = <从 Vercel 控制台 Blob 页面拿>

# 4. 部署并触发
tcb fn deploy vercel-blob-migrate -e <envId>
tcb fn invoke vercel-blob-migrate -e <envId>

方式 B:作为 CloudBase 云托管(⭐⭐ 大体量推荐)

无超时限制,适合几十万~百万级文件。把脚本封装为 Express HTTP 接口,每次接收 prefix + cursor 参数,返回已迁移数 + 下一批 cursor,前端或定时任务按需续跑。

mkdir vercel-blob-migrate-runner && cd vercel-blob-migrate-runner
npm init -y
npm i @vercel/blob @cloudbase/js-sdk express
# 把 migrateVercelBlob 改为:app.post('/migrate', async (req, res) => { ... })
# server.js 启动监听 process.env.PORT || 3000

tcb cloudrun deploy -e <envId> --source .
# 部署后访问 https://<service>.ap-shanghai.run.tcloudbase.com/migrate?prefix=&cursor=

方式 C:本地运行(一次性小批量)

适合千级以内文件的快速迁移,需要本地能访问 Vercel 和 CloudBase API(境外网络可能需要代理)。

mkdir vercel-blob-migrate && cd vercel-blob-migrate
npm i @vercel/blob @cloudbase/js-sdk
# 把脚本贴为 vercel-blob-migrate.js
BLOB_READ_WRITE_TOKEN=<token> node vercel-blob-migrate.js

Node 18+(需要原生 fetch)。

4. 业务代码改动要点

  1. 文件 ID 格式:Vercel 用 https://xxx.blob.vercel-storage.com/... URL 作为引用,CloudBase 用 cloud://<envId>.<bucket>/<path> 形式 fileID,需要在数据库里做一次批量替换。
  2. 私有读访问:Vercel get() 返回 ReadableStream,CloudBase 没有等价方法,私有读文件必须先 getTempFileURL 获取临时 HTTPS 链接(默认 2 小时,可设 maxAge),再让前端请求。
  3. 公开读控制:Vercel 通过 access: 'public' | 'private' 写在文件上,CloudBase 是目录级权限——在「云存储 → 权限设置」勾选「公有读」即可永久直链,等同 Vercel 的 access: 'public'
  4. 客户端直传:Vercel 用 @vercel/blob/clientupload() + 服务端 handleUpload(),CloudBase 直接在浏览器用 app.uploadFile() 一行完成(需登录态或匿名登录)。

模块五:身份认证

CloudBase 内置身份认证:开箱即用微信开放平台、手机短信、邮箱密码、匿名、自定义登录 Ticket 五种登录源;自带用户管理后台、Token 签发与刷新、登录态持久化、Cloud SDK 直接读取用户身份。免去自建 Auth 服务或对接第三方平台的运维成本。控制台在「用户管理 → 登录方式」。

选型背景:Vercel 生态的第三方 Auth 如何选择

Vercel 本身不提供身份认证(Vercel 是部署平台,不是 Auth 服务)。在 Vercel 上做登录,开发者通常从以下三家选一家:

方案定位适合什么情况
NextAuth(现 Auth.js)开源库,Next.js 团队官方维护Next.js 项目首选,create-next-app 默认带,零配置接入
Clerk托管式 SaaS,开箱即用 UI 组件需要开箱即用的登录 UI 和用户管理后台,无需自行开发登录页面
Auth0企业级身份认证平台(Okta 旗下)企业合规、SSO、复杂权限模型、生态集成广泛

迁移到 CloudBase 后,上面三种方案都可以直接替换为 CloudBase 内置认证——免维护外部依赖、自带用户管理后台、微信/短信开箱即用。

1. API 映射

第三方 Auth 常见操作CloudBase 内置认证(@cloudbase/js-sdk说明
获取当前登录用户auth.currentUser / (await auth.getSession()).data.session.user各家命名不同(Clerk useUser()、NextAuth getSession()、Auth0 getUser()),功能等价
邮箱密码登录auth.signInWithPassword({ email, password })Clerk signIn.create()、NextAuth signIn("credentials")、Auth0 loginWithPassword()
手机验证码登录auth.signInWithOtp({ phone })data.verifyOtp({ token })仅支持上海地域三家均不内置短信登录能力,需接 Twilio 等外部服务;CloudBase 开箱即用
第三方 OAuth(Google/GitHub 等)auth.signInWithCustomTicket(() => Promise.resolve(ticket))实现方式不同:CloudBase 不直接提供 OAuth Provider,改为后端生成 Ticket 桥接(见下方「自定义登录」)
退出登录auth.signOut()一致
获取 Token / 会话(await auth.getSession()).data.sessionCloudBase 用自有 Token(SDK 内部管理),不暴露标准 JWT;取用户 ID 用 data.session.user.id、取 Token 用 data.session.access_token,传给自有业务后端时作为参数传入,云函数侧用 app.auth().getUserInfo() 校验
用户管理后台控制台「用户管理」(禁用 / 重置密码 / 查登录日志)Clerk/Auth0/NextAuth 各有后台,迁移后统一走 CloudBase 控制台

2. 登录方式代码示例

启用顺序:先在控制台「用户管理 → 登录方式」勾选要开启的登录方式,再调用对应 SDK 方法。

以下代码在浏览器端运行(前端页面 / React / Vue 组件中),需先安装:

# Step 1:安装 SDK
npm i @cloudbase/js-sdk
// Step 2:在前端代码中使用
import cloudbase from '@cloudbase/js-sdk'

const app = cloudbase.init({ env: 'your-env-id' })
const auth = app.auth

// 1. 匿名登录(无注册快速体验)
await auth.signInAnonymously()

// 2. 短信验证码登录(仅支持上海地域)
const { data } = await auth.signInWithOtp({ phone: '13800138000' })
await data.verifyOtp({ token: '123456' })

// 3. 邮箱密码登录
await auth.signInWithPassword({ email: 'user@example.com', password: 'pwd123' })

// 4. 微信开放平台 H5 登录
await auth.signInWithWechat()

// 5. 自定义登录 Ticket(桥接您现有的 NextAuth/Clerk/Auth0 用户体系)
// 注意:v3 的 signInWithCustomTicket 参数是「函数」而不是「对象」
await auth.signInWithCustomTicket(() => fetch('/api/get-ticket').then(r => r.text()))

注意:调用任意 auth.* 方法前,需先在控制台「用户管理 → 登录方式」勾选对应登录源,否则会报「登录方式未启用」错误。

3. 存量用户迁移

CloudBase 没有「批量导入」控制台,需要写一个 import-users 云函数逐个导入。

操作流程:

  1. 从 Clerk/Auth0/NextAuth 后台导出用户为 JSON/CSV:
  2. 在云函数中调用 auth.signUpWithEmailAndPassword() 逐条写入 CloudBase。

部署步骤

# Step 1:从 Clerk/Auth0/NextAuth 后台导出用户 JSON/CSV,整理为 [{ email, password }, ...]

# Step 2:上传到 CloudBase 云存储,获取 fileID
# 控制台「云存储 → 上传」→ 拷贝 fileID(cloud://<envId>.<bucket>/xxx.json)

# Step 3:创建云函数(使用 @cloudbase/js-sdk v3)
mkdir -p cloudfunctions/import-users && cd cloudfunctions/import-users
npm init -y
npm i @cloudbase/js-sdk
# 把下面代码写入 index.js
// cloudfunctions/import-users/index.js
const cloudbase = require('@cloudbase/js-sdk').default
// 云函数内不传 env 自动取当前环境
const app = cloudbase.init({ env: process.env.CLOUDBASE_ENV_ID })

async function importUsers(users) {
for (const u of users) {
try {
// @cloudbase/js-sdk v3 signUp 方法
await app.auth.signUpWithEmailAndPassword({
email: u.email,
password: u.password,
})
console.log(`${u.email}`)
} catch (e) {
console.warn(`${u.email}: ${e.message}`)
}
}
}

exports.main = async (event) => {
const { fileList } = await app.getTempFileURL({
fileList: [event.fileID || 'cloud://xxx.json'],
})
const res = await fetch(fileList[0].tempFileURL)
const users = await res.json()
await importUsers(users)
return { imported: users.length }
}
# Step 4:部署 + 触发
tcb fn deploy import-users -e <envId>
tcb fn invoke import-users -e <envId> --params '{"fileID":"cloud://xxx.json"}'

密码哈希不能直接迁移——Clerk/Auth0 用 bcrypt/argon2 哈希,CloudBase 无法直接导入原系统的密码哈希值。推荐做法:迁移时强制重置密码,在用户首次登录时引导走"忘记密码 → 邮件链接"流程。短信/微信登录不受此影响,用户在新系统里用验证码/微信扫码即可完成迁移。

4. 第三方 OAuth 桥接(Google / GitHub / 已有 NextAuth)

CloudBase Web SDK 不直接接第三方 OAuth,但有"自定义登录 Ticket"通道——您后端继续维护 NextAuth/Clerk 会话,签一个短期 Ticket 给前端,前端用 signInWithCustomTicket 换取 CloudBase 登录态。架构示意:

浏览器 → 您的 Next.js / CloudBase 云函数
├─ 已登录 NextAuth?→ 用 jose/jsonwebtoken 签 Ticket(HS256,secret 在环境变量)
└─ 未登录?→ 重定向到 Google/GitHub OAuth
浏览器 ← Ticket
浏览器 → CloudBase auth.signInWithCustomTicket({ ticket })
CloudBase → 返回 CloudBase 登录态 + uid

云函数侧签发 Ticket 的最小示例(用 jose 库):

部署步骤

# Step 1:创建云函数目录
mkdir -p cloudfunctions/get-ticket && cd cloudfunctions/get-ticket
npm init -y
npm i jose

# Step 2:把下面代码写入 index.js
// cloudfunctions/get-ticket/index.js
const { SignJWT } = require('jose')
const secret = new TextEncoder().encode(process.env.CUSTOM_LOGIN_SECRET)

exports.main = async (event) => {
// 1. 校验您自己的会话(NextAuth 的 cookie、Clerk 的 token 等)
const userId = event.userId // 从您原系统里拿到
if (!userId) throw new Error('not signed in')

// 2. 签一个短期 Ticket(5 分钟内有效)
const ticket = await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('5m')
.sign(secret)

return { ticket }
}
# Step 3:配置环境变量
# 控制台「云函数 → get-ticket → 函数配置 → 环境变量」添加:
# CUSTOM_LOGIN_SECRET = <用 openssl rand -hex 32 生成的值>

# Step 4:部署 + 配 HTTP 触发器(让前端能 GET 获取 ticket)
tcb fn deploy get-ticket -e <envId>
# 控制台「云函数 → get-ticket → 触发管理 → 创建触发器」选「HTTP 触发器」

# Step 5:前端调用(v3 需传函数,SDK 会按需拉取)
const { ticket } = await fetch('https://<envId>.ap-shanghai.app.tcloudbase.com/get-ticket').then(r => r.json())
await auth.signInWithCustomTicket(() => Promise.resolve(ticket))

模块六:环境变量

在 CloudBase 控制台配置环境变量,替换原有的 Vercel 环境变量。代码层完全兼容——process.env 读取方式不变:

VercelCloudBase说明
Dashboard → Settings → Environment Variables控制台 → 云函数 / 云托管 → 环境变量按函数/服务分别配置
vercel env addcloudbaserc.jsonenvVariables 字段或直接控制台配置
vercel env pull.env 文件 + tcb CLI本地开发读取
process.env.*process.env.*代码层完全一致,无需改动

迁移脚本工具

2 个脚本覆盖「配置 → 函数」迁移链。直接复制脚本到本地,赋予可执行权限(chmod +x *.js)即可使用。

脚本用途使用方式
migrate-vercel-config.js解析 vercel.json,输出 CloudBase 对应配置node migrate-vercel-config.js [vercel.json](默认 ./vercel.json
batch-migrate-functions.js批量把 api/ 下的 Vercel Functions 转为云函数 / 云托管node batch-migrate-functions.js ./api [输出目录] --target=function(云函数模式,默认)
node batch-migrate-functions.js ./api [输出目录] --target=cloudrun(云托管 Express 模式)

端到端迁移流程

# 0. 前置:安装并登录 CLI
npm install -g @cloudbase/cli
tcb login # 登录 CloudBase

# 1. 转换 vercel.json 配置
node migrate-vercel-config.js vercel.json
# → 终端输出 rewrites/redirects/headers/crons 的 CloudBase 对应做法

# 2. 批量生成骨架(二选一)
# 云函数模式 → 每个 Function 一个独立云函数
node batch-migrate-functions.js ./api --target=function
# → 输出到 cloudfunctions-migrated/<funcName>/,逐个适配后 tcb fn deploy

# 云托管模式 → 合并为一个 Express 服务(推荐函数多时使用)
node batch-migrate-functions.js ./api --target=cloudrun
# → 输出到 cloudfunctions-migrated/ 含 server.js + Dockerfile,tcb cloudrun deploy 一键部署

1. 转换 vercel.jsonmigrate-vercel-config.js

解析 vercel.json 的 6 类配置(rewrites / redirects / headers / builds / functions / crons),逐项给出 CloudBase 对应做法。终端输出,可直接复制粘贴到控制台或代码里。

#!/usr/bin/env node
// 用法: node migrate-vercel-config.js [vercel.json 路径]
const fs = require('fs')
const filePath = process.argv[2] || './vercel.json'
if (!fs.existsSync(filePath)) { console.error('❌ 文件不存在'); process.exit(1) }
const config = JSON.parse(fs.readFileSync(filePath, 'utf-8'))

// --- 1. rewrites ---
if (config.rewrites) {
const rewrites = Array.isArray(config.rewrites) ? config.rewrites : []
const hasSPAFallback = rewrites.some(r =>
r.source === '/((?!api|_next|.*\\..*).*)' || r.source === '/(.*)'
)
if (hasSPAFallback) {
console.log('📌 【SPA Fallback】')
console.log(' 👉 控制台 → 静态网站托管 → 基础配置 → 错误页面')
console.log(' 将「4xx 错误页面」设为: index.html')
console.log('')
}
if (rewrites.length) {
console.log(' 提示:复杂 rewrites 建议使用 CloudBase 云托管(Express/Koa)实现')
console.log('')
}
}

// --- 2. redirects ---
if (config.redirects) {
const redirects = Array.isArray(config.redirects) ? config.redirects : []
console.log('📌 【redirects → 静态托管路由规则 / 云托管中间件】')
for (const rule of redirects.slice(0, 5)) {
const status = rule.statusCode || (rule.permanent ? 308 : 307)
console.log(` ${rule.source}${rule.destination} (${status})`)
}
console.log('')
console.log(' 👉 轻量:控制台「路由规则」配置')
console.log(' 👉 推荐:云托管 Express 中间件:')
console.log('')
console.log(' // server.js')
for (const rule of redirects.slice(0, 3)) {
const src = rule.source.replace(/\\//g, '')
const dest = rule.destination
const code = rule.statusCode || 301
console.log(` app.get('${src}', (req, res) => res.redirect(${code}, '${dest}'))`)
}
console.log('')
}

// --- 3. headers ---
if (config.headers) {
const headers = Array.isArray(config.headers) ? config.headers : []
console.log('📌 【headers → 静态托管响应头 / 云托管】')
console.log(' 👉 轻量:控制台「HTTP 响应头」配置')
console.log(' 👉 推荐:云托管 Express 中间件:')
console.log('')
console.log(' app.use((req, res, next) => {')
for (const rule of headers.slice(0, 3)) {
for (const h of rule.headers || []) {
console.log(` res.setHeader('${h.key}', '${h.value}')`)
}
}
console.log(' next()')
console.log(' })')
console.log('')
}

// --- 4. builds / functions ---
if (config.builds || config.functions) {
console.log('📌 【builds / functions → CI/CD 中指定,无需迁移】')
console.log(' 构建命令: npm run build')
if (config.buildCommand) console.log(` (原 Vercel 配置: ${config.buildCommand})`)
console.log('')
}

// --- 5. crons ---
if (config.crons) {
console.log('📌 【crons → 云函数定时触发器】')
for (const cron of config.crons) {
console.log(` 路径: ${cron.path} 调度: ${cron.schedule}`)
// Vercel 5 位 cron → CloudBase 7 位 (补秒和年)
const parts = cron.schedule.split(' ')
const cloudbaseCron = parts.length === 5
? `0 ${parts[0]} ${parts[1]} ${parts[2]} ${parts[3]} ${parts[4]} *`
: cron.schedule
console.log(' CloudBase 云函数 config.json:')
console.log(' {')
console.log(' "triggers": [{')
console.log(` "name": "${cron.path.replace(/\//g, '_').replace(/^_/, '')}",`)
console.log(' "type": "timer",')
console.log(` "config": "${cloudbaseCron}"`)
console.log(' }]')
console.log(' }')
console.log('')
}
}

if (!config.rewrites && !config.redirects && !config.headers && !config.builds && !config.functions && !config.crons) {
console.log('ℹ️ 未检测到需要转换的 Vercel 配置项')
}
console.log('✅ 转换建议输出完成')

2. 批量转换 Vercel Functions(batch-migrate-functions.js

扫描 api/ 目录(递归),识别动态路由 [param].ts / [...slug].ts。支持两种输出模式:

  • --target=function(默认):每个 Vercel Function → 一个独立云函数骨架(index.js + package.json
  • --target=cloudrun:所有 Functions 合并为一个 Express 云托管服务,自动注册路由 + Dockerfile + .dockerignore
#!/usr/bin/env node
// 用法:
// 云函数模式: node batch-migrate-functions.js <api目录> [输出目录] --target=function
// 云托管模式: node batch-migrate-functions.js <api目录> [输出目录] --target=cloudrun
const fs = require('fs')
const path = require('path')

// ----- 参数解析 -----
const args = process.argv.slice(2).filter(a => !a.startsWith('--target='))
const targetArg = process.argv.find(a => a.startsWith('--target='))
const target = (targetArg ? targetArg.split('=')[1] : 'function').toLowerCase()
const sourceDir = args[0] || './api'
const outputDir = args[1] || './cloudfunctions-migrated'

if (!['function', 'cloudrun'].includes(target)) {
console.error('❌ --target 只能是 function 或 cloudrun,当前值: ' + target)
process.exit(1)
}
if (!fs.existsSync(sourceDir)) { console.error('❌ 源目录不存在: ' + sourceDir); process.exit(1) }

// ----- 公共工具 -----
function walkDir(dir, basePath = '') {
const files = []
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name)
const relativePath = path.join(basePath, entry.name)
if (entry.isDirectory()) files.push(...walkDir(fullPath, relativePath))
else if (/\.(ts|js|mjs)$/.test(entry.name)) files.push({ fullPath, relativePath })
}
return files
}

function parseDynamicRoute(fileName) {
const m = fileName.match(/^\[(.+?)\]\.(?:ts|js|mjs)$/)
if (!m) return null
const param = m[1]
return param.startsWith('...')
? { name: param.slice(3), wildcard: true }
: { name: param, wildcard: false }
}

// ----- 路由名生成(共用)-----
function generateFunctionName(p) {
return p.replace(/\.[tj]s$/, '').replace(/\[\.{3}/g, '').replace(/[\[\]]/g, '')
.replace(/[\/\\]/g, '-').replace(/^-/, '') || 'index'
}

// ================================================================
// 模式一: --target=function(逐个云函数)
// ================================================================
function generateFunctionSkeleton(relativePath, originalCode) {
const funcName = generateFunctionName(relativePath)
const dr = parseDynamicRoute(path.basename(relativePath))
return `// ============================================================
// CloudBase 云函数: ${funcName}
// 源文件: ${relativePath}
// ============================================================
// ⚠️ 由迁移脚本自动生成,需手动适配
// 原 Vercel Function 代码(已注释,供参考):
// ============================================================
${originalCode.split('\n').map(l => '// ' + l).join('\n')}
// ============================================================
// CloudBase 云函数模板:
// 入参 event = { httpMethod, path, queryStringParameters, headers, body }
// 返回 { statusCode, headers, body }
// ============================================================
${dr ? `// 动态路由参数: ${dr.wildcard ? '**' + dr.name : ':' + dr.name}` : ''}
exports.main = async (event, context) => {
const method = event.httpMethod || 'GET'
const params = event.queryStringParameters || {}
const headers = event.headers || {}
${dr ? 'const routeParam = event.pathParameters || {}' : ''}
let body = {}
if (event.body) {
try { body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body }
catch { body = {} }
}
// ===== 在此处实现原始 Vercel Function 的逻辑 =====
// req.query.xxx → params.xxx
// req.body.xxx → body.xxx
// res.status(n).json(data) → return { statusCode: n, body: JSON.stringify(data) }
// ================================================
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify({ message: 'Function migrated from Vercel. TODO: implement logic.', method, params }),
}
}
`
}

function migrateAsFunctions(files, outputDir) {
for (const { fullPath, relativePath } of files) {
const funcName = generateFunctionName(relativePath)
const funcDir = path.join(outputDir, funcName)
fs.mkdirSync(funcDir, { recursive: true })
fs.writeFileSync(path.join(funcDir, 'package.json'), JSON.stringify({
name: `cb-${funcName}`, version: '1.0.0',
description: `Migrated from Vercel API route: ${funcName}`,
main: 'index.js', dependencies: {},
}, null, 2))
fs.writeFileSync(path.join(funcDir, 'index.js'), generateFunctionSkeleton(relativePath, fs.readFileSync(fullPath, 'utf-8')))
console.log(`${relativePath}${funcName}/`)
}
console.log('')
console.log('下一步:')
console.log(' 1. 逐个打开 index.js 适配业务逻辑')
console.log(' 2. cd ' + outputDir + '/<funcName> && npm install')
console.log(' 3. tcb fn deploy <funcName> -e <envId> --dir ' + outputDir + '/<funcName>')
}

// ================================================================
// 模式二: --target=cloudrun(合并为 Express 云托管)
// ================================================================
function toExpressPath(relativePath) {
// hello.ts → /api/hello
// users/index.ts → /api/users
// users/[id].ts → /api/users/:id
// posts/[...slug].ts → /api/posts/*
let p = relativePath.replace(/\/index\.[tj]s$/, '').replace(/\.[tj]s$/, '')
p = p.replace(/\[\.\.\.(.+?)\]/g, '*') // [...slug] → *
p = p.replace(/\[(.+?)\]/g, ':$1') // [id] → :id
return '/api/' + p
}

function generateRouteFile(relativePath, originalCode) {
const routePath = toExpressPath(relativePath)
return `// ============================================================
// 路由: ${routePath}
// 源文件: ${relativePath}
// ============================================================
// ⚠️ 由迁移脚本自动生成,需手动适配
// 原 Vercel Function 代码(已注释,供参考):
// ============================================================
${originalCode.split('\n').map(l => '// ' + l).join('\n')}
// ============================================================
// Express handler: req/res 是标准 Express 对象
// req.params.xxx → 动态路由参数
// req.query.xxx → Query 参数
// req.body → 已 parse 的 JSON body
// res.status(n).json(data) → 直接使用 Express API
// ============================================================

module.exports = async (req, res) => {
const { method, query, params, body, headers } = req

// ===== 在此处实现原始 Vercel Function 的逻辑 =====
// req.query.xxx → query.xxx
// req.body.xxx → body.xxx
// req.params.xxx → params.xxx (动态路由参数)
// ================================================

return res.json({
message: 'Route migrated from Vercel. TODO: implement logic.',
method,
params,
query,
})
}
`
}

function generateServerJs(files) {
const requires = files.map(f => {
const routePath = toExpressPath(f.relativePath)
const importName = generateFunctionName(f.relativePath).replace(/-/g, '_')
return `const ${importName} = require('./routes/${f.relativePath.replace(/\\/g, '/').replace(/\.[tj]s$/, '.js')}')`
})

const registrations = files.map(f => {
const routePath = toExpressPath(f.relativePath)
const importName = generateFunctionName(f.relativePath).replace(/-/g, '_')
let regCode
if (routePath.includes('*')) {
// wildcard: app.all('/api/posts/*', handler)
regCode = `app.all('${routePath}', ${importName})`
} else {
regCode = `app.all('${routePath}', ${importName})`
}
return ` ${regCode}`
})

return `// ============================================================
// CloudBase 云托管 Express 入口
// 由 batch-migrate-functions.js --target=cloudrun 自动生成
// ============================================================
const express = require('express')
const app = express()

// ---- 基础中间件 ----
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') return res.sendStatus(200)
next()
})

// ---- 路由注册(来自 api/ 下的 Vercel Functions)----
${requires.join('\n')}

${registrations.join('\n')}

// ---- 健康检查 ----
app.get('/health', (req, res) => res.json({ status: 'ok' }))

// ---- 错误处理 ----
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).json({ error: err.message || 'Internal Server Error' })
})

// ---- 启动 ----
const PORT = process.env.PORT || 3000
app.listen(PORT, () => console.log(\`CloudBase CloudRun listening on :\${PORT}\`))
`
}

function migrateAsCloudRun(files, outputDir) {
const routesDir = path.join(outputDir, 'routes')
fs.mkdirSync(routesDir, { recursive: true })

// 写入各路由文件
for (const { fullPath, relativePath } of files) {
const routeFile = path.join(routesDir, relativePath.replace(/\\/g, '/').replace(/\.[tj]s$/, '.js'))
fs.mkdirSync(path.dirname(routeFile), { recursive: true })
fs.writeFileSync(routeFile, generateRouteFile(relativePath, fs.readFileSync(fullPath, 'utf-8')))
console.log(' ✅ ' + relativePath + ' → routes/' + relativePath.replace(/\.[tj]s$/, '.js'))
}

// server.js
fs.writeFileSync(path.join(outputDir, 'server.js'), generateServerJs(files))

// package.json
fs.writeFileSync(path.join(outputDir, 'package.json'), JSON.stringify({
name: 'vercel-migrated-api',
version: '1.0.0',
description: 'Migrated from Vercel API routes to CloudBase CloudRun',
main: 'server.js',
scripts: { start: 'node server.js' },
dependencies: { express: '^4.18.2' },
}, null, 2))

// Dockerfile
fs.writeFileSync(path.join(outputDir, 'Dockerfile'), [
'FROM node:18-alpine',
'WORKDIR /app',
'COPY package*.json ./',
'RUN npm install --production',
'COPY . .',
'EXPOSE 3000',
'CMD ["node", "server.js"]',
'',
].join('\n'))

// .dockerignore
fs.writeFileSync(path.join(outputDir, '.dockerignore'), [
'node_modules',
'.env',
'.git',
'README.md',
'',
].join('\n'))

console.log('')
console.log('下一步:')
console.log(' 1. 逐个打开 routes/*.js 适配业务逻辑')
console.log(' 2. cd ' + outputDir + ' && npm install && npm start # 本地联调')
console.log(' 3. tcb cloudrun deploy -e <envId> --source ' + outputDir + ' # 一键部署云托管')
}

// ================================================================
// 主流程
// ================================================================
const files = walkDir(sourceDir)
const targetLabel = target === 'cloudrun' ? '(云托管 Express)' : '(独立云函数)'
console.log('📂 扫描到 ' + files.length + ' 个文件 → 输出到 ' + outputDir + ' ' + targetLabel)
console.log('')

if (target === 'cloudrun') {
migrateAsCloudRun(files, outputDir)
} else {
migrateAsFunctions(files, outputDir)
}

常见问题

Q:Vercel 上的 SPA History 路由(访问 /page/sub 刷新返回 404)如何处理?

在 CloudBase 静态托管控制台的「基础配置」中,将 错误页面 设为 index.html,即可实现类似 Nginx try_files 的 SPA 路由回退效果。

Q:Vercel 的 Preview Deployments 在 CloudBase 如何替代?

使用多个 CloudBase 环境(dev / staging / prod),每个环境绑定不同分支的 CI/CD 流水线。

Q:Vercel Functions 与 CloudBase 云函数的超时和内存如何对比?

云函数最长 900 秒(vs Vercel Pro 60s),云托管无超时限制,大规模任务直接选云托管。

Q:迁移后性能会下降吗?

CloudBase 使用腾讯云 CDN 全球加速节点,国内访问速度优于 Vercel(Vercel 国内无节点)。海外访问也覆盖全球边缘节点。