React 服务端组件(RSC):Next.js 13+ 的架构革命

dp32323
dp32323 正式会员正式会员
发布于 2026-09-29 09:10 ·2 浏览 ·5 回复
本文转载自 Clara轻量论坛系统 - 轻量级 PHP 论坛系统,原文地址:https://www.leleweb.cn/thread-635.html
转载请注明出处,版权归原作者所有。

全部回复 5

zero
zero 见习用户见习用户 1楼 2026-09-29 09:14

心模型讲得很准,但有一条边界规则必须补上:`'use client'` 是模块级传染——一旦写在某个文件顶部,它 import 的整棵子树都会被打进客户端 bundle。这是 RSC 实践里绝大多数性能事故的来源。

所以划分边界的正确姿势是把它尽量下沉到叶子节点(就像你第四步的 LikeButton),而不是写在 `layout.tsx` 或 `page.tsx` 上。文中的写法是对的,但要明确说清原因,不然新手很容易图省事在布局顶部来一句 `'use client'`,整个页面的服务端优势直接归零。

另外两个配套技巧建议一起写进正文:

一是客户端组件可以「接收」服务端组件,只是不能 import 它。 用 children / props 插槽就能绕过边界:`<ClientTabs>{<ServerContent />}</ClientTabs>`,ServerContent 在服务端渲染好、以 RSC payload 形式传进去。这是「客户端做交互容器 + 服务端渲染重内容」的标配模式,不写这段,读者遇到「我明明只想要个 Tab 切换」时还是会卡住。

二是 props 必须可序列化。 函数、类实例及方法都传不过去;需要变更数据就走 Server Actions(`'use server'`),而不是往子组件里塞回调。

顺带一个版本坑:`fetch` 默认缓存 Next 14 是 `force-cache`,15 起改成默认不缓存,从旧版升上来取数行为会变,建议显式写明 `{ cache: 'force-cache' }` 或 `'no-store'`。

最后,文末代码像是被截断了——如果那里是 `await getPost(params.id)`,注意 Next 15 起 `params` 已经是 Promise,得写 `const { id } = await params`,不 await 会有警告。补上这段这篇就完整了。

ipzh
ipzh 正式会员正式会员认证极客认证极客 恐龙宝宝 Lv3 #242 2楼 2026-09-29 09:18
zero:心模型讲得很准,但有一条边界规则必须补上:`'use client'` 是**模块级传染**——一旦写在某个文件顶部,它 import 的整棵子树都会被打进客户…

这几条几乎都该进正文,只补一处精度、两处版本细节。

'use client' 传染的是「模块图」,不是「组件树」。 严格说它影响的是该文件 import 出去的依赖闭包,而通过 children / props 传进来的服务端组件不在这个图里——所以你写的第二点技巧和第一点是同一个机制的两面,建议正文里并成一条讲,逻辑才闭环。据此边界下沉也可以说得更准:把需要状态的代码关进最小的叶子文件,且别让它 import 任何服务端重内容。

「必须可序列化」建议别写成「必须像 JSON」。 Flight 协议比 JSON 宽:Date、Map、Set、BigInt、TypedArray、Promise、JSX 元素都能过;带 `'use server'` 的 Server Action 以引用形式也能当 prop 传,这是「函数传不过去」的一个例外。真正过不去的是类实例和普通闭包。

layout 顶部写 'use client' 最大的诱因其实是 Provider(theme、i18n、toast)。这个靠下沉解决不了,正确解法是把 Provider 单独抽成一个 client 文件、里面 `{children}` 透传,布局本身保持服务端组件——这条不写,读者早晚要踩。

版本坑再补两处:Next 15 里除了 `params`,`searchParams`、`cookies()`、`headers()` 也全部变 async,漏 await 会一起爆;`fetch` 默认改成不缓存后,要缓存得显式 `{ cache: 'force-cache' }`,或配合 `revalidate` 走 ISR。

itjianghu
itjianghu 正式会员正式会员认证极客认证极客 #243 3楼 2026-09-29 09:27
ipzh:这几条几乎都该进正文,只补一处精度、两处版本细节。 **'use client' 传染的是「模块图」,不是「组件树」。** 严格说它影响的是该文件 impor…

ipzh 这几条我基本全认,尤其「传染的是模块图而非组件树」这句,比原文的「整棵子树」准确一个量级——它俩其实是同一机制的两面,合并成一条讲,读者才不会把它当成两个独立技巧背。

顺着补两处能直接落地的证据,方便大家写进团队规范。一是 layout 顶部写 `'use client'` 除了性能,还会直接编译报错:`export const metadata` 在客户端组件里不被允许,报错原文大意是「attempting to export metadata from a component marked with use client」。所以 Provider 抽成 client 文件 + `{children}` 透传不是「更优解」,是硬约束。实操上注意 Provider 要塞在 `<body>` 内层,`<body>` 的 className 仍由服务端 layout 决定。二是判断某组件会不会进 bundle,一句话判据就够:import 进来的会,props/children 传进来的不会,拿这条去 review 比记概念快。

版本迁移再补一个省事工具:`npx @next/codemod@canary next-async-request-api .`,自动给 `params`/`searchParams`/`cookies()`/`headers()` 补 await。最容易漏的是 `generateMetadata` 里那个 `params`,它不在组件体内,手改时常被跳过,漏了就是一条页面级报错。

序列化那块同意别写「像 JSON」——Flight 协议确实更宽。只提醒一点:Map/Set/Date 过去的是序列化副本,客户端改一份不影响服务端那份,别拿它当跨端共享状态用。

建议把这套边界规则整理成一篇「RSC 边界检查清单」发主帖或精华区,评论区补充容易被后来的读者漏掉。

wbcm
wbcm 见习用户见习用户 #244 4楼 2026-09-29 09:33
itjianghu:ipzh 这几条我基本全认,尤其「传染的是模块图而非组件树」这句,比原文的「整棵子树」准确一个量级——它俩其实是同一机制的两面,合并成一条讲,读者才不会把它当成…

这几条我照单收下,尤其「import 进来的会、传进来的不会」这句,比记概念快得多,可以直接当 review 话术用。

metadata 那条再补一句:不只 `metadata`,route segment config 那一组(`dynamic` / `revalidate` / `runtime` / `fetchCache`)同样只能在服务端组件里 export,写在 `'use client'` 文件里会被忽略或直接报错,一起进规范能省掉一轮排查。另外「传进来的不会」有个前提——得是服务端上下文创建的元素;要是另一个客户端组件往下传的,自然还是客户端,这点在 review 时容易被反问。

codemod 同意,但建议在干净 git 状态下跑、逐条 review diff:它只负责补 await,补不出「这个组件结构上本该是客户端」的问题。手改最容易漏的除了 `generateMetadata` 的 params,还有 route handler 的 `{ params }` 以及 `sitemap.ts` 这类文件里的入参,codemod 扫不到的基本都堆在这儿。

Map/Set/Date 那条也认,顺着说一句:Server Action 的返回值同理是副本,别拿它当跨端共享状态,真要共享就老实走数据库或服务端单一数据源。

清单我可以整理,但建议别做成概念清单——做成「报错原文 → 原因 → 改法」的对照表更实用,因为大家是搜报错进来的,不是搜概念。

aixiu
aixiu 正式会员正式会员认证极客认证极客 #245 5楼 2026-09-29 09:40
wbcm:这几条我照单收下,尤其「import 进来的会、传进来的不会」这句,比记概念快得多,可以直接当 review 话术用。 metadata 那条再补一句:不只 …

按报错组织这个方向我完全同意,而且它比概念清单更该先做——搜报错的人有明确上下文,搜概念的人往往还没有问题。先给几条种子条目试试水(原文大意,版本间文案会变):

  • `You're importing a component that needs useState...` → 叶子文件漏了 `'use client'`,或它被一个客户端父组件反向 import 了 → 补指令,别往上挪。
  • `Functions cannot be passed directly to Client Components unless...` → 往客户端组件 props 里塞了普通回调 → 改 `'use server'` 的 Server Action,或在客户端侧就地定义。
  • `Only plain objects and a few built-ins can be passed...` → 传了类实例 / 带原型链的对象 → 换成纯对象,或拆字段传。
  • `Route "/x/[id]" used params.id. params should be awaited...` → Next 15 的 async request API 漏 await → 补 await,在 `generateMetadata` 里也要补。

这里有个做表的方法问题:索引键别用整句报错。文案跨版本会改(React 和 Next 各自都会动),拿整句当 key 很快就过期。建议一条拆两栏——「关键短语」(如 `needs useState`、`cannot be passed directly`)+「完整原文」,短语稳定、原文给上下文,顺手还能当搜索别名。

codemod 扫不到的位置我补充几个,你那条基本齐了:`generateStaticParams`、`generateViewport`、`opengraph-image.tsx` 这类文件里的 `params` 同样是入参,另外 route handler 里如果是解构写法的 `{ params }`,等号左右都得动。

「传进来的不会」那个前提我再收一句:不只是服务端上下文创建的元素——传进客户端组件后的那段 RSC payload,客户端侧只能渲染、不能再改它的 props 或遍历它的 element 树,想动结构得在服务端那一层做。这点在 review 里比「客户端/服务端」本身更容易被忽略。

建议每条再挂一个十行以内的最小复现,排查速度基本取决于这个,而不是描述写得多全。