React Router 服务端 RSC 路由与 SSR 请求分发:unstable_routeRSCServerRequest 实战解析
【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router
导读
unstable_routeRSCServerRequest是 React Router 在Data(数据)模式下打通 React Server Components(RSC)与浏览器/SSR 两端的"中枢":它接收进入服务器的原生Request,将其交给 RSC 服务器处理后,对数据 / 资源请求原样代理返回序列化 RSC 载荷(payload),对文档请求则把载荷渲染为可下发给浏览器的 HTML 文档。读完本文,你将掌握该 API 的签名、全部参数语义、典型 SSR 入口(entry.ssr.tsx)写法、CSP nonce 注入方式,以及它内部的请求分流、重定向、流式注入与错误重试原理。该 API 目前为实验性(unstable)特性,其完整定义位于源码 packages/react-router/lib/rsc/server.ssr.tsx,可结合 RSCStaticRouter 文档、React Server Components 指南 一起阅读。
它在 RSC 架构中的位置
React Router 将 RSC 场景拆成三个入口(详见 React Server Components how-to 中 "Entry points" 一节):
entry.rsc.tsx(React Server):把请求与路由匹配并生成 RSC 载荷,对应matchRSCServerRequest;entry.ssr.tsx(SSR Server / 请求处理):处理请求、调用 RSC 服务器、在文档请求时把 RSC 载荷渲染成 HTML——routeRSCServerRequest正是这个入口的核心 API;entry.browser.tsx(浏览器):水合 HTML 并设置callServer以支持水合后的服务端动作。
注意:文档强调并不需要物理上部署两个服务器,更常见的是在同一个服务器内维护两条独立的模块图——React 在"生成 RSC 载荷"与"生成可水合的 HTML"时的行为是不同的。这在 entry.rsc.tsx 示例 中有直观体现:RSC 入口通过
import.meta.viteRsc.loadModule加载 SSR 入口的generateHTML,先调 RSC 服务器拿到Response,再交给 SSR 侧渲染 HTML。
函数签名
async function routeRSCServerRequest({ request, serverResponse, createFromReadableStream, renderHTML, hydrate = true, nonce, }: { request: Request; serverResponse: Response; createFromReadableStream: SSRCreateFromReadableStreamFunction; renderHTML: ( getPayload: () => DecodedPayload, options: { nonce?: string; onError(error: unknown): string | undefined; onHeaders(headers: Headers): void; }, ) => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>; hydrate?: boolean; nonce?: string; }): Promise<Response>各参数的核心语义如下(与 routeRSCServerRequest.md 中的 Params 一节一致):
| 参数 | 类型 | 说明 |
|---|---|---|
request | Request | 待路由分发的原始请求(浏览器或 fetch 客户端发来的那个请求)。 |
serverResponse | Response | 由 RSC 处理器生成、包含序列化unstable_RSCPayload的Response(或部分响应)。 |
createFromReadableStream | SSRCreateFromReadableStreamFunction | 你的react-server-dom-xyz/client提供的createFromReadableStream,用于在服务器端解码 RSC 流载荷。 |
renderHTML | (getPayload, options) => ReadableStream<Uint8Array> | 把 RSC 载荷渲染为 HTML 的函数,通常配合<RSCStaticRouter>使用。 |
hydrate | boolean(默认true) | 是否把 RSC 载荷注入进 HTML 以便客户端水合;置为false可仅渲染(如用于纯 SSG/ISR 输出)。 |
nonce | string(可选) | 渲染 HTML 时生成的内联脚本所需的 CSP nonce。 |
返回值
返回值是标准Response:
- 对数据 / 资源请求,直接返回包含 RSC 载荷(
unstable_RSCPayload)的响应; - 对文档请求,返回渲染好的 HTML 响应(
text/html)。
典型用法:自定义 SSR 入口
官方示例中,routeRSCServerRequest通常被封装在 SSR 入口导出的generateHTML(request, serverResponse)内。下面是根据 routeRSCServerRequest.md 与 React Server Components how-to 的 Vite 示例 整理的完整可运行写法:
import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; import * as ReactDomServer from "react-dom/server.edge"; import { unstable_RSCStaticRouter as RSCStaticRouter, unstable_routeRSCServerRequest as routeRSCServerRequest, } from "react-router"; export async function generateHTML( request: Request, serverResponse: Response, ): Promise<Response> { return await routeRSCServerRequest({ // 传入的原始请求。 request, // RSC 服务器生成的响应。 serverResponse, // React Server 的解码触点。 createFromReadableStream, // 把 router 渲染成 HTML。 async renderHTML(getPayload, options) { const payload = await getPayload(); const formState = payload.type === "render" ? await payload.formState : undefined; // 可选的 bootstrap 脚本内容(如内联启动配置)。 const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); return await ReactDomServer.renderToReadableStream( <RSCStaticRouter getPayload={getPayload} />, { ...options, bootstrapScriptContent, formState, signal: request.signal, }, ); }, }); }注意三个同名参数在此各司其职:
getPayload():触发对 RSC 流的解码,返回一个"扩展版"载荷 Promise。从源码看,这个对象上还挂有_deepestRenderedBoundaryId(供错误边界定位)和formState(表单状态)两个访问器,见 server.ssr.tsx 的getPayload实现;options.nonce:由routeRSCServerRequest注入的 CSP nonce;options.onError/options.onHeaders:分别用于拦截渲染错误与收集 React 生成的响应头。
RSC Framework 模式下的内置默认 SSR 入口(@react-router/dev/config/default-rsc-entries/entry.ssr)本质就是上述模板的官方实现,可查看 default-rsc-entries/entry.ssr.tsx 作为对照;仓库中的 playground/rsc-vite/src/entry.ssr.tsx 与 integration/helpers/rsc-vite/src/entry.ssr.tsx 也提供了真实可跑的同款入口代码。
参数深入:hydrate 与 nonce
hydrate(默认true)
决定最终的 HTML 响应里是否注入可水合的 RSC 载荷脚本。从源码看(server.ssr.tsx):
- 当
hydrate为true时,HTML 流会经过injectRSCPayload(serverResponseB.body, { nonce })转换,把 RSC 流切分成一段段<script>注入</body>之前; - 当
hydrate为false时,HTML 直接原样输出(仅追加可能的重定向 meta 标签)。因此,如果你要用 RSC 做SSG / ISR 纯预渲染而不需要客户端水合,应关闭hydrate。
nonce
RSC 载荷的传输依赖 HTML 文档中的内联脚本(形如(self.__FLIGHT_DATA||=[]).push(...))。如果你的站点启用了 CSP,就必须为这些内联脚本生成一次性 nonce。规范做法是:为每个文档响应生成一个新的 nonce,并同时传给routeRSCServerRequest、<RSCStaticRouter>以及 CSP 响应头,详见下面的安全章节。
底层原理:请求分流与响应代理
从源码实现(server.ssr.tsx)可看出routeRSCServerRequest的核心决策逻辑。它首先对 URL 与请求头做三类判断:
const url = new URL(request.url); const isDataRequest = isReactServerRequest(url); // url.pathname.endsWith(".rsc") const respondWithRSCPayload = isDataRequest || isManifestRequest(url) || // url.pathname.endsWith(".manifest") request.headers.has("rsc-action-id"); // 服务端动作(Server Action)请求随后:
- 数据 / 资源请求直接代理:若命中上述任一判断,或者
serverResponse带有React-Router-Resource: true头,则原样返回serverResponse——此时浏览器拿到的是纯 RSC 流,由客户端侧RSCHydratedRouter配合createFromReadableStream解码并继续 SPA 导航。 - 文档请求才渲染 HTML:其余请求会进入下面的 HTML 渲染管线。
这套"按 URL 后缀/请求头 + 响应头分流"的设计同时服务于框架路由懒发现(manifest 请求)与 Server Action(rsc-action-id),是保持"一次请求、一套数据"的关键。
重定向识别
渲染前会用克隆的响应先解码一次载荷,识别 React Router 特有的single-fetch 重定向状态码(SINGLE_FETCH_REDIRECT_STATUS)与type === "redirect"的载荷,此时会剥离编码相关头(Content-Encoding、Content-Length、Content-Type、X-Remix-Response)并改写为真正的Location重定向(server.ssr.tsx)。源码中还通过hasInvalidProtocol校验重定向地址的协议合法性,避免开放重定向。
流式注入与 HTML 结尾重定向
HTML 渲染完成后:
- 输出
Content-Type: text/html; charset=utf-8,合并 React 头与 RSC 服务器头; - 若发生了重定向,会在 HTML 流
flush阶段追加<meta http-equiv="refresh" content="0;url=...">(并做 HTML 转义),保证即使 HTML 已开始流式输出也能完成跳转; - 若
hydrate=true,HTML 会通过injectRSCPayload把 RSC 载荷以<script>块形式流式写入</body>之前。
injectRSCPayload实现在 lib/rsc/html-stream/server.ts:它会在每个事件循环 tick 聚合 HTML 分块,避免在 HTML 半截块中间错误插入脚本;对无法 UTF-8 解码的二进制块退化为 base64 编码的Uint8Array.from(...);同时对 payload 脚本内容做escapeScript、对 nonce 属性做escapeAttribute转义。相关行为在测试tests/rsc/html-stream-test.ts 中都有覆盖,例如:
- "streams buffered HTML, RSC payload chunks, and the HTML trailer" 断言输出形如
<html><body>hi<script>(self.__FLIGHT_DATA||=[]).push("S1:\"hello\"")</script></body></html>; - nonce 转义测试(
nonce: 'test"&<>')断言每个 payload 脚本都带转义后的nonce属性; - 客户端中途取消(cancel)场景下不会产生未处理的 Promise rejection。
渲染失败的一次性重试
renderHTML并非"一次失败就放弃"。当首次渲染因边界错误抛错时(源码通过decodeRedirectErrorDigest/decodeRouteErrorResponseDigest解析 React 错误 digest),routeRSCServerRequest会结合已渲染出的最深层错误边界 id(_deepestRenderedBoundaryId),把归一化后的status、errors合并进载荷并重新执行一次renderHTML,把错误边界渲染进最终 HTML(见 server.ssr.tsx 中的重试分支)。若renderHTML抛出的本身就是Response(例如RSCStaticRouter遇重定向载荷抛出的响应对象),则直接原样返回该响应。
实战:CSP nonce 的安全配置
默认框架入口并不生成 nonce,只有在你同时下发 CSP 响应头时才需要生成。在 React Server Components how-to 的 "Content Security Policy nonces" 一节 给出了完整范式:在entry.ssr.tsx中为每个文档请求用crypto.randomUUID()生成新 nonce,并让 nonce 贯穿三个位置——routeRSCServerRequest选项、RSCStaticRouter的nonceprop,以及 CSP 响应头:
export async function generateHTML( request: Request, serverResponse: Response, ): Promise<Response> { const nonce = crypto.randomUUID(); const response = await routeRSCServerRequest({ request, serverResponse, createFromReadableStream, nonce, async renderHTML(getPayload, options) { const payload = getPayload(); const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); return renderHTMLToReadableStream( <RSCStaticRouter getPayload={getPayload} nonce={options.nonce} />, { ...options, bootstrapScriptContent, formState: await payload.formState, signal: request.signal, }, ); }, }); response.headers.set( "Content-Security-Policy", `script-src 'self' 'nonce-${nonce}'`, ); return response; }这里 nonce 的分工是:
routeRSCServerRequest的nonce选项:作用于传输 RSC 载荷的内联脚本(最终由injectRSCPayload逐个写进<script nonce="...">);- 展开
...options传入renderHTMLToReadableStream:作用于 React 自身生成的脚本; - 传给
<RSCStaticRouter>:成为<Links>、<ScrollRestoration>等 nonce-aware 组件的默认 nonce。
仓库集成测试 integration/rsc-nonce-test.ts 验证了该 nonce 链路在真实 Vite 环境中的行为。文档同时提醒:对静态预渲染页面,应优先使用 CSP hash 或外部脚本,而不是按响应生成 nonce。
适用范围与限制
- 本文 API 标注为
unstable,属于实验性能力:可能在任何 minor/patch 版本中发生破坏性变更,使用时需格外谨慎并密切跟进 CHANGELOG.md 中的相关变更。 - 只适用于Data 模式(文档头部标注
[MODES: data]);在 RSC Framework 模式下它已封装进框架自带/可覆盖的entry.ssr.tsx,通常无需直接调用,但了解其原理有助于你理解框架模式下的 SSR 输出。 - 若无需 SSR / 客户端水合,也可只取它的 HTML 生成能力用于静态站点生成,此时记得关闭
hydrate。
结合 RSCStaticRouter 文档(它负责真正把解码出的载荷渲染为 HTML 静态路由树)与 matchRSCServerRequest 文档(载荷的生成端),你即可在自己的 Vite/自定义框架中复刻出一套完整的 RSC + SSR 双模块图请求链路。
【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考