从 Vercel 迁移
将 Vercel 部署的项目(静态站点、Next.js、Serverless Functions 等)迁移至腾讯云开发 CloudBase
迁移总览
Vercel 和 CloudBase 在核心能力上高度对齐,都提供前端 + Serverless 后端 + 数据存储的一站式部署体验。迁移主要围绕 静态托管、后端计算(云函数 & 云托管)、数据库、文件存储、身份认证、环境变量 六个模块逐项适配。
| 对比维度 | Vercel | CloudBase |
|---|---|---|
| 静态托管 | 自动从 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 Storage | CloudBase 云存储 |
模块一:静态托管
CloudBase 静态网站托管:内置 CDN、HTTPS、SPA Fallback、HTTP 响应头、路由规则。控制台在「静态网站托管」。
1. 纯静态站点(Vite / Vue / React / Svelte / Astro / Hugo)
适用:构建产物是纯 HTML/CSS/JS,不依赖 Vercel Functions
# 国际站用户请先执行 tcb config set isIntl true(或设置环境变量 TCB_IS_INTL=true)切换到国际站链路
# 安装 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.js | next.config.js 中设置 output: 'export' | tcb app deploy --framework next --output-dir out -e <envId> |
| Nuxt.js | nuxt.config.ts 中设置 ssr: false | tcb app deploy -e <envId> |
| SvelteKit | svelte.config.js 中配置 @sveltejs/adapter-static | tcb 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 Functions | CloudBase 云函数 |
|---|---|---|
| 运行环境 | Node.js / Go / Python / Ruby | Node.js / PHP / Python / Golang / Java |
| 触发方式 | HTTP 路径自动路由 | HTTP 触发 / 定时触发 / 数据库触发 |
| 路径映射 | api/*.ts → /api/* | 每个函数一个路径 |
| 超时时间 | 10s(Hobby)/ 60s(Pro) | 最长 900s |
| 内存 | 128MB - 3008MB | 256MB - 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_dump含CREATE FUNCTION ... $$ ... $$;或COPY ... FROM stdin;时,按;拆分 SQL 的逻辑可能出错,建议改用--inserts模式导出纯 INSERT 语句,或在脚本中增加$$块识别。
模块四:文件迁移
CloudBase 云存储:内置 CDN 域名、免费 HTTPS、图片处理(缩略图/裁剪/水印)、安全规则细粒度控制。控制台在「存储 → 云存储」。
CloudBase 承接能力:Vercel Blob 是对象存储,CloudBase 对应的是云存储——内置 CDN 域名、免费 HTTPS、图片处理(缩略图/裁剪/水印)、安全规则细粒度控制。Blob 文件本身的迁移脚本在下方给出,存量业务的「上传/读取」代码改动主要在 API 替换。
1. API 映射
Vercel Blob(@vercel/blob) | CloudBase 云存储(@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 SDK 用 storage.listDirectoryFiles(cloudPath[, limit, marker]) | 浏览器端需业务侧维护文件清单;Node.js 脚本或云函数中可使用 @cloudbase/manager-node 拉取;也可在控制台「存储 → 文件管理」手动导出 |
head(path) | 无原生批量 API;Manager Node SDK 用 storage.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. 业务代码改动要点
- 文件 ID 格式:Vercel 用
https://xxx.blob.vercel-storage.com/...URL 作为引用,CloudBase 用cloud://<envId>.<bucket>/<path>形式 fileID,需要在数据库里做一次批量替换。 - 私有读访问:Vercel
get()返回ReadableStream,CloudBase 没有等价方法,私有读文件必须先getTempFileURL获取临时 HTTPS 链接(默认 2 小时,可设maxAge),再让前端请求。 - 公开读控制:Vercel 通过
access: 'public' | 'private'写在文件上,CloudBase 是目录级权限——在「云存储 → 权限设置」勾选「公有读」即可永久直链,等同 Vercel 的access: 'public'。 - 客户端直传:Vercel 用
@vercel/blob/client的upload()+ 服务端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.session | CloudBase 用自有 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 云函数逐个导入。
操作流程:
- 从 Clerk/Auth0/NextAuth 后台导出用户为 JSON/CSV:
- 在云函数中调用
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 读取方式不变:
| Vercel | CloudBase | 说明 |
|---|---|---|
| Dashboard → Settings → Environment Variables | 控制台 → 云函数 / 云托管 → 环境变量 | 按函数/服务分别配置 |
vercel env add | cloudbaserc.json 中 envVariables 字段 | 或直接控制台配置 |
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.json(migrate-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 国内无节点)。海外访问也覆盖全球边缘节点。