Next.js RSC 数据边界:哪些数据到浏览器,哪些状态留在服务端

用文章详情与编辑操作划分 Server Component、Client Component 和 Server Action 的职责,说明 DTO、请求瀑布、缓存配置及写入后可见性。

·6 min
服务器保留数据库与密钥,仅将必要页面数据跨边界传给浏览器中的交互控件。

文章详情页读取正文,收藏按钮响应点击,编辑表单保存修改。页面把三件事放在一起,数据却不该整块从数据库流到浏览器。

设计 RSC 边界时,先确认权威数据位于哪里,再列出浏览器完成交互所需的字段。以下示例针对 Next.js 16 App Router,写法按随包 16.3.3 官方指南核对;缓存配置仍取决于实际部署。

先定义公开数据

Server Component 可以在服务端读取数据。但只要把一个对象作为 props 传给 Client Component,其中的字段就可能进入发送给浏览器的序列化结果。客户端没有把某个字段显示在页面上,不意味着用户无法取得它。

例如,文章详情的公开组件只需要标题、正文与发布时间,收藏按钮只需要文章 ID 和用户的收藏状态。审核备注、内部作者信息和草稿内容不应随数据库对象一起传下去。

可以先定义公开数据形状:

type PublicArticle = {
  id: string;
  slug: string;
  title: string;
  bodyMarkdown: string;
  publishedAt: string;
};

这个接口选择 ISO 日期字符串,方便调用方理解和传输。React 能接受的值不止传统 JSON 类型;这里要求的是经过授权、可序列化的最小字段集合。Server 与 Client Components 指南说明了 props 与模块边界。

数据访问函数还应检查内容确实处于可公开状态。只根据 slug 查出一条记录,再让页面决定隐藏哪些字段,会让多个入口各自重复安全逻辑。更清楚的约定是:readPublicArticle 只返回可公开对象,否则返回空值。服务端模块可用 server-only 限制误导入,但这个标记不会阻止开发者主动把秘密放入 props。

让交互保持局部

Server Component 读取文章,再嵌入一个小型收藏控件。示例中的读取函数和控件由应用实现:

import { notFound } from 'next/navigation';
import { readPublicArticle } from '@/lib/article-reader';
import BookmarkButton from '@/components/bookmark-button';

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const article = await readPublicArticle(slug);
  if (!article) notFound();

  return (
    <article>
      <h1>{article.title}</h1>
      <time dateTime={article.publishedAt}>
        {article.publishedAt}
      </time>
      <BookmarkButton articleId={article.id} />
    </article>
  );
}

示例省略正文渲染,只展示读取和交互边界;正式页面还需要安全的 Markdown 渲染流程。BookmarkButton 的登录态与收藏状态应由它自己的明确数据契约处理,不能仅凭拿到文章 ID 就允许任意写入。

'use client' 标记模块依赖边界。把它加在整个布局上,会让更多直接导入的模块进入客户端依赖图。服务端父组件生成的内容仍可作为 children 传给客户端组件,组件在视觉上的嵌套关系并不决定模块归属。

客户端组件也可能参与首屏的服务端 HTML 预渲染。因此不能仅凭文件有 'use client' 就在渲染顶层读取 window;浏览器专属操作仍需放到合适的生命周期或事件中。

按依赖关系安排读取

服务端直接读取本应用的数据访问层,通常不必绕一次自己的 HTTP API。额外的自调用会增加网络边界,也让鉴权、错误处理和部署地址更复杂。若数据本来属于另一个服务,则继续通过它的接口访问,不能为了减少一次请求破坏服务边界。

独立的数据可以并发读取。例如文章主体与公开推荐如果互不依赖,可以同时开始;推荐依赖文章分类时,则至少先取得分类。对无权访问的文章,不应提前把受限数据送进其他服务。

Suspense 可以让不同区域分阶段呈现,减轻一个慢区域对其他内容的阻塞。它不会消除底层数据库开销,也不会自动去重任意 ORM 查询。请求内复用、跨请求缓存和浏览器数据缓存需要分别设计。

客户端收到完整首屏数据后,不应在挂载时无条件再拉取同一份数据。需要轮询或主动刷新时,应说明它对应的时效需求,并把服务端给出的初始数据接入客户端状态管理。否则一次页面访问可能执行两套相互覆盖的读取流程。

明确缓存范围

“在服务端读取”不等于“跨请求缓存”。fetch 的数据缓存、路由预渲染结果和浏览器导航缓存也不能合并理解。按所用模式检查 fetch 官方语义,不要沿用旧版本默认值的记忆。

Next.js 16 的 Cache Components 模式以 use cache 等接口组织缓存。启用后,原来的部分路由段配置会被替代,详见 迁移指南。未启用的应用应沿用对应的缓存模型,避免把两种模式的配置拼在一起。

设计上,公开正文可以按内容版本缓存,用户收藏状态则与身份有关。私人数据若进入共享缓存,缓存键必须体现有效访问范围;更稳妥的起点是先保持该读取独立,再基于实际需求增加缓存。

修改文章之后,详情、列表、搜索结果和其他公开出口可能同时需要更新。应列出每个入口的缓存所有者和允许滞后时间。仅刷新当前页面,不一定使其他持有旧数据的读者获得新版本;撤回内容还需要考虑旧缓存继续披露的问题。

让 Server Action 负责写入

编辑按钮只对作者可见,不能替代服务端权限校验。Server Action 每次执行都要验证身份、目标文章和操作权限,校验输入,并在事务中保持业务约束。绑定到表单的 ID 或闭包中的值,都不能直接视为可信授权依据。官方 数据安全指南将数据访问层和写入入口的安全要求分开说明。

保存成功后,应区分数据库已提交、缓存已失效和界面已显示新值这几个状态。若数据库提交成功、后续刷新失败,界面不能诱导用户把同一业务动作当作全新请求重做。返回结果和重试语义要与写入操作一同设计。

RSC 能帮助把读取靠近数据源,但不会替团队决定授权、缓存和一致性。先给这些边界命名,才能判断一次重复请求是否多余、一个缓存是否安全,以及一次保存之后用户应该看到什么。