在 Next.js Server Components 中读写 CloudBase 云数据库
一句话定义:用
@cloudbase/node-sdk在 Next.js App Router 的 Server Component 里直接await db.collection().get(),写操作走 Server Action('use server')再revalidatePath刷新缓存,Client Component 只负责表单和触发,不感知数据库。预计耗时:30 分钟 | 难度:进阶
适用场景
- 适用:全栈 Next.js 项目想直接连 CloudBase 数据库,不想另起一套 API 路由层
- 适用:已经在用 RSC(React Server Components),想让数据获取下沉到组件树里
- 适用:已经有 CloudBase 后端(小程序 / 移动端共用),Web 端用 Next.js 重写
- 不适用:纯前端 SPA / 静态导出(
output: 'export')— 走 add-auth-web-with-cloudbase-sdk + 客户端 SDK - 不适用:小程序内业务 — 走 add-database-wechat-miniprogram
- 不适用:整个 Next.js 部署到 Cloudflare Workers / Edge Runtime —
@cloudbase/node-sdk依赖 Node 内置模块,跑不了 Edge
环境要求
| 依赖 | 版本 |
|---|---|
| Node.js | ≥ 18.17(Next.js 14 / 15 最低要求) |
next | ^14.x 或 ^15.x(App Router) |
@cloudbase/node-sdk | 最新版(npm view @cloudbase/node-sdk version) |
| 部署运行时 | Node Runtime(默认),不能是 Edge Runtime |
另外:
- 已开通的 CloudBase 环境 ID
- 一对腾讯云 API 密钥(
SecretId/SecretKey),控制台 → 访问管理 CAM 申请,权限至少包含QcloudTCBFullAccess(或更细粒度的QcloudTCBReadWriteAccess) - 一个测试用集合
users,集合权限设置成「仅创建者及管理员可读写」,防止客户端 SDK 绕过 Server 直接读写
@cloudbase/node-sdk 持的是腾讯云 API 密钥,等同 admin 身份,绝对不能 import 进任何 Client Component('use client' 文件)。Next.js 默认会把 Client Component 的依赖打包到客户端 JS,密钥会随包发出去。本篇所有 @cloudbase/node-sdk 的 import 都只出现在 Server Component / Server Action / Route Handler 里。
第一步:lib/cloudbase.ts 共享 init
tcb.init 内部维护连接池,每个文件都 init 一次会浪费;统一放到一个共享模块,惰性初始化:
npm i @cloudbase/node-sdk
lib/cloudbase.ts:
import tcb from '@cloudbase/node-sdk';
let cached: ReturnType<typeof tcb.init> | null = null;
export function getCloudbase() {
if (!cached) {
cached = tcb.init({
env: process.env.CLOUDBASE_ENV!,
secretId: process.env.TENCENTCLOUD_SECRETID,
secretKey: process.env.TENCENTCLOUD_SECRETKEY,
});
}
return cached;
}
.env.local(本地开发,不要 commit):
CLOUDBASE_ENV=your-env-id
TENCENTCLOUD_SECRETID=AKIDxxxxxx
TENCENTCLOUD_SECRETKEY=xxxxxxxx
要点:
- 没加
'use server'/'use client'标记 — 这是个普通 ES module,被谁 import 由谁决定运行时;本篇只让 Server 侧 import 它 - 三个环境变量名字按腾讯云官方推荐:
TENCENTCLOUD_SECRETID/TENCENTCLOUD_SECRETKEY,在腾讯云 Cloud Run / 云函数等托管平台是默认注入的,本地用.env.local模拟 - 部署到 Vercel / 自建服务器:在平台的环境变量面板里配同名变量
第二步:Server Component 直接 await 数据库
App Router 里的 page.tsx 默认是 Server Component,可以直接 async,顶层 await 数据库,渲染同步出 HTML:
app/users/page.tsx:
import { getCloudbase } from '@/lib/cloudbase';
export default async function UsersPage() {
const app = getCloudbase();
const db = app.database();
const { data } = await db.collection('users').limit(10).orderBy('createdAt', 'desc').get();
return (
<main>
<h1>用户列表</h1>
<ul>
{data.map((u) => (
<li key={u._id}>
{u.name} — {new Date(u.createdAt).toLocaleString('zh-CN')}
</li>
))}
</ul>
</main>
);
}
要点:
- Server Component 里直接
await,Next.js 会等查询完才发 HTML,首屏没有 loading 闪烁 db.collection().get()返回{ data, requestId, ... },只关心数据时取.data即可- 想让查询并行:
const [a, b] = await Promise.all([q1.get(), q2.get()]) - 这个文件没有
'use client',默认是 RSC,没有useState/useEffect这些客户端 hook 可用
第三步:Server Action 写数据
写操作放到独立的 actions.ts,顶部 'use server' 标记 — Next.js 会把这个文件标成「永远在 Server 跑」,Client Component import 时只会拿到一个 RPC 句柄,不会把代码打包进客户端。
app/users/actions.ts:
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { getCloudbase } from '@/lib/cloudbase';
export async function createUser(formData: FormData) {
const name = (formData.get('name') as string)?.trim();
if (!name) {
return { ok: false, error: '名字不能为空' };
}
try {
const app = getCloudbase();
const db = app.database();
await db.collection('users').add({
data: {
name,
createdAt: Date.now(),
},
});
} catch (e: any) {
return { ok: false, error: e.message ?? 'unknown error' };
}
// 让 /users 路径下的 Server Component 重新跑一次查询
revalidatePath('/users');
return { ok: true };
}
要点:
'use server'必须是文件顶部第一行(可以在注释之后),不能写到函数里- Server Action 必须是
async function,参数和返回值都要是 可序列化 的 —FormData/ 基本类型 / 普通对象 OK,函数 / class 实例 /Date对象都不行(Date序列化后变字符串,所以例子里写成Date.now()数字) - 错误别
throw抛给客户端,客户端拿到的是被 Next.js 包过的通用错误,信息不全;返回{ ok, error }形式 revalidatePath('/users')让该路径下所有 RSC 缓存作废,下次请求重新跑;不调这个,新数据要等到下次硬刷新才看见
app/users/CreateForm.tsx(Client Component):
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createUser } from './actions';
const initialState = { ok: false, error: '' };
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '创建中...' : '创建'}
</button>
);
}
export function CreateForm() {
const [state, formAction] = useFormState(createUser, initialState);
return (
<form action={formAction}>
<input name="name" placeholder="名字" required />
<SubmitButton />
{state?.error && <span style={{ color: 'red' }}>{state.error}</span>}
</form>
);
}
要点:
<form action={createUser}>是 React 19 / Next.js 14+ 的写法,表单提交直接走 Server Action,无需写onSubmit+fetchuseFormState把 Server Action 返回值塞进 React state,可以渲染错误信息useFormStatus拿到提交中的pending状态,用来禁用按钮 — 注意它必须用在<form>子组件里,不能和useFormState写在同一个组件- React 19 起
useFormState改名useActionState(从react而不是react-domimport),Next.js 15 + React 19 走新名字
把表单挂到 page.tsx:
import { getCloudbase } from '@/lib/cloudbase';
import { CreateForm } from './CreateForm';
export default async function UsersPage() {
const { data } = await getCloudbase().database().collection('users').limit(10).orderBy('createdAt', 'desc').get();
return (
<main>
<h1>用户列表</h1>
<CreateForm />
<ul>
{data.map((u) => (
<li key={u._id}>{u.name}</li>
))}
</ul>
</main>
);
}
第四步:revalidatePath / revalidateTag 刷新缓存
Next.js 的 RSC 默认会做「请求级缓存」+「Data Cache」:同一次渲染里相同的 fetch 会去重,部署后页面会被 build 成静态产物。revalidatePath / revalidateTag 是主动让缓存作废的两种方式。
// 路径级别:作废所有访问 /users 的渲染缓存
revalidatePath('/users');
// 动态路由作废:第二参数说明类型
revalidatePath('/users/[id]', 'page');
// 标签级别:批量作废
import { revalidateTag } from 'next/cache';
revalidateTag('users');
如果想让数据库查询配合 tag,封装一层 unstable_cache:
// lib/users.ts
import { unstable_cache } from 'next/cache';
import { getCloudbase } from './cloudbase';
export const getUsers = unstable_cache(
async () => {
const { data } = await getCloudbase().database().collection('users').limit(10).get();
return data;
},
['users-list'],
{ tags: ['users'], revalidate: 60 }, // 60s 自动 stale,或被 revalidateTag('users') 主动作废
);
Server Action 里写完后调 revalidateTag('users') 即可同时作废所有挂了 users tag 的查询。
如果只有一个页面查 users,直接 revalidatePath 就够,引 unstable_cache 反而增加复杂度。多个页面共用查询、或者要做「定时重新查」的时候再上 unstable_cache。
第五步:权限规则配置
CloudBase 数据库有 6 种内置权限规则,Server-side admin 操作不受集合权限限制(API 密钥本来就是 admin),但为了防止客户端 SDK 直接绕过 Server,集合权限要设成最严格的:
控制台 → 数据库 → 你的集合 → 权限设置 → 选 「仅创建者及管理员可读写」 或 「自定义安全规则」:
{
"read": "auth.uid != null && doc._openid == auth.uid",
"write": "auth.uid != null && doc._openid == auth.uid"
}
业务层做用户身份校验:Server Action 里要拿当前用户身份的话,有两条路:
方案 A — 业务层自己做用户系统(推荐)
在 Next.js 里用 next-auth / 自建 session,Server Action 里读 cookie 拿 userId,作为业务字段写入文档:
'use server';
import { cookies } from 'next/headers';
import { getCloudbase } from '@/lib/cloudbase';
export async function createUser(formData: FormData) {
const session = cookies().get('session')?.value;
const userId = await verifySession(session); // 你自己的校验逻辑
if (!userId) return { ok: false, error: '未登录' };
const db = getCloudbase().database();
await db.collection('users').add({
data: { name: formData.get('name'), ownerId: userId, createdAt: Date.now() },
});
// ...
}
方案 B — 借用 CloudBase 用户体系
如果已经有 CloudBase 用户(比如和小程序共用),Server 端把客户端的 token 换成 CloudBase 身份:
const app = getCloudbase();
const auth = app.auth();
const ticket = await auth.getUserInfo({ accessToken: clientToken });
// ticket.uid 就是 CloudBase 的 uid
实操中绝大多数 SaaS 走方案 A — Server 拿 admin 权限做所有操作,业务层自己定义谁能改谁,数据库只是个被锁死的仓库。
第六步:错误处理 boundary
Server Component 里 throw 的错误会 冒泡到最近的 error.tsx,可以为每个路由段建一个错误边界:
app/users/error.tsx:
'use client'; // error.tsx 必须是 Client Component
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>加载用户失败</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}
如果是「找不到」的语义,Server Component 里直接调 notFound(),跳到 not-found.tsx:
import { notFound, redirect } from 'next/navigation';
export default async function UserDetail({ params }: { params: { id: string } }) {
const { data } = await getCloudbase().database().collection('users').doc(params.id).get();
if (!data || data.length === 0) notFound();
// 没权限看?跳走
if (data[0].private) redirect('/login');
return <div>{data[0].name}</div>;
}
notFound() / redirect() 内部是 throw 一个特殊错误对象,Next.js 框架捕获后跳页;不要 把它们包进 try/catch,会被吃掉。
运行验证
- 本地:
npm run dev起 Next.js,访问http://localhost:3000/users,页面应该直接渲染出空列表(初次没有数据)和创建表单 - 在表单里输入名字,提交,1 秒内列表应该出现新条目(
revalidatePath触发了 RSC 重渲染) - 控制台 → 数据库 → users 集合,能看到刚才提交的记录,字段
name/createdAt都对 - 关掉 dev server,在浏览器开 Network 面板,刷新页面 — 看到
users这个 document 的响应是完整 HTML,数据已经渲染进去,不是后续 fetch 拿到的(说明 SSR 工作正常) - 在浏览器 Console 里搜
TENCENTCLOUD_SECRETID或你的密钥前缀(比如AKID),应该一个都搜不到 — 如果搜到说明有 Client Component 误 import 了lib/cloudbase.ts,立刻排查 - 部署到 Vercel / 腾讯云 Cloud Run,在平台面板配置
CLOUDBASE_ENV/TENCENTCLOUD_SECRETID/TENCENTCLOUD_SECRETKEY三个环境变量,重新部署后再走一次 1-5
常见错误
| 错误 / 现象 | 原因 | 修复 |
|---|---|---|
客户端 Console 看到 process is not defined 或一堆 Node 内置模块报错 | 在 Client Component('use client' 文件)里 import 了 lib/cloudbase.ts 或 @cloudbase/node-sdk | 把数据库调用挪到 Server Component / Server Action,Client Component 只调 Server Action;或者在 next.config.js 里把 @cloudbase/node-sdk 加到 serverExternalPackages 强制 server-only |
Server Action 调用报 Server Actions must be async functions | actions.ts 里某个 export 不是 async | 所有 'use server' 文件里的 export 必须是 async function;同步函数不行 |
Server Action 调用报 Cannot pass non-serializable parameter | 给 Action 传了 Date 实例、class 实例、函数等 | 改用基本类型 / FormData / 普通对象;Date 改成 .getTime() 数字或 ISO 字符串 |
revalidatePath('/users') 调了但页面没刷新 | 路径写错(写成 /Users 大小写,或漏了动态段) | 路径必须和文件路由一致;动态路由用 revalidatePath('/users/[id]', 'page') |
部署后报 The edge runtime does not support Node.js | 路由文件加了 export const runtime = 'edge',或部署到 Edge Runtime 平台 | @cloudbase/node-sdk 必须 Node Runtime;移除 runtime = 'edge',或改用 Cloudbase 的客户端 SDK 走 HTTP |
数据库查询超时 / connect ETIMEDOUT | 部署环境出口 IP 没配进腾讯云 / 或 SDK 走错域名 | CloudBase 默认走公网,公有云部署不需要白名单;如果是企业内网部署,确认能访问 tcb-api.tencentcloudapi.com |
| 控制台「数据库」里看到客户端 SDK 直接读到了数据 | 集合权限太松(留了「所有用户可读」) | 改为「仅创建者及管理员可读写」或自定义规则,Server admin 不受影响 |
| 登录态相关 — 用户身份信息丢失 | 试图在 @cloudbase/node-sdk 里维护用户登录态 | node-sdk 是 admin 身份,不持用户态;用户态走 Next.js 自己的 cookie / session,见第五步方案 A |
错误码定义参考 error-code。
相关文档
- Node.js SDK 数据库 API —
db.collection().add/get/update/remove完整签名 - Node.js SDK 初始化 —
tcb.init参数详解 - 集合权限规则 — 6 种内置规则 + 自定义安全规则语法
- add-database-wechat-miniprogram — 端对照:小程序里同一个集合怎么操作
- add-vercel-ai-sdk-streaming-chatbot — Next.js + AI SDK 的姊妹篇,可以拼着用
- deploy-nextjs-to-cloudbase-run — 把这个 Next.js 项目部署到腾讯云 Cloud Run
- secure-database-multi-tenant-rules — 多租户场景的权限规则进阶
下一步
- 接 CloudBase 用户体系做登录:Web 端走 add-auth-web-with-cloudbase-sdk 的客户端 SDK 拿 token,Server 端用 token 校验
- 数据变更实时推送给 Web 端:add-realtime-notifications-database-watch
- 高频写入场景的查询优化:optimize-database-query-performance