跳到主要内容

在 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.js18.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 + fetch
  • useFormState 把 Server Action 返回值塞进 React state,可以渲染错误信息
  • useFormStatus 拿到提交中的 pending 状态,用来禁用按钮 — 注意它必须用在 <form> 子组件里,不能和 useFormState 写在同一个组件
  • React 19 起 useFormState 改名 useActionState(从 react 而不是 react-dom import),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 的查询。

简单场景不用 unstable_cache

如果只有一个页面查 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,会被吃掉。

运行验证

  1. 本地:npm run dev 起 Next.js,访问 http://localhost:3000/users,页面应该直接渲染出空列表(初次没有数据)和创建表单
  2. 在表单里输入名字,提交,1 秒内列表应该出现新条目(revalidatePath 触发了 RSC 重渲染)
  3. 控制台 → 数据库 → users 集合,能看到刚才提交的记录,字段 name / createdAt 都对
  4. 关掉 dev server,在浏览器开 Network 面板,刷新页面 — 看到 users 这个 document 的响应是完整 HTML,数据已经渲染进去,不是后续 fetch 拿到的(说明 SSR 工作正常)
  5. 在浏览器 Console 里搜 TENCENTCLOUD_SECRETID 或你的密钥前缀(比如 AKID),应该一个都搜不到 — 如果搜到说明有 Client Component 误 import 了 lib/cloudbase.ts,立刻排查
  6. 部署到 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 functionsactions.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

相关文档

下一步