react-router StaticRouterProvider 深度解析:静态数据路由器的 SSR 渲染与 Hydration 数据原理
在 React 应用中做服务端渲染(SSR)时,<StaticRouterProvider> 是 react-router 数据路由(Data Router)模式下打通“服务器 → 浏览器”这条链路的核心组件。它接收由 createStaticHandler 发起数据请求后得到的 StaticHandlerContext 与静态 DataRouter,在服务端一次性渲染出完整的 HTML,并把 loader/action 数据以脚本形式嵌入页面,供客户端水合时直接复用。读完本文,你将掌握 StaticRouterProvider 的完整用法、四个 props 的精确语义、Hydration 脚本的双重序列化与安全转义机制,以及它在无状态导航环境下的实现细节。
定位:一个“不能导航”的 DataRouter
官方文档(StaticRouterProvider.md)对它的定义非常简洁:
A
DataRouterthat may not navigate to any otherLocation. This is useful on the server where there is no stateful UI.
即:它是一个不允许跳转到任何其他 Location 的数据路由器,适用于没有有状态 UI 的服务器环境。它的完整签名是:
function StaticRouterProvider({
context,
router,
hydrate = true,
nonce,
}: StaticRouterProviderProps)
文档给出的标准用法(来自 JSDoc,见 server.tsx 的 @example)如下:
export async function handleRequest(request: Request) {
let { query, dataRoutes } = createStaticHandler(routes);
let context = await query(request);
if (context instanceof Response) {
return context;
}
let router = createStaticRouter(dataRoutes, context);
return new Response(
ReactDOMServer.renderToString(<StaticRouterProvider ... />),
{ headers: { "Content-Type": "text/html" } }
);
}
这个示例浓缩了完整的 SSR 请求处理流水线,值得逐行理解:
createStaticHandler(routes):创建静态处理器,返回{ query, queryRoute, dataRoutes }。其中query()面向文档请求(document requests),queryRoute()面向定向路由请求,dataRoutes是内部转换后的数据路由对象(签名与参数详见 createStaticHandler.md,实现位于 router.ts)。await query(request):执行 loader/action 数据请求。返回值有两种可能:StaticHandlerContext:正常渲染所需的数据上下文;Response实例:通常是重定向或 loader 抛出的Response(例如throw redirect("/login"))。此时应直接把该Response作为当前请求的返回值,不再渲染。
createStaticRouter(dataRoutes, context):用query的返回上下文构造静态路由器(签名见 createStaticRouter.md)。renderToString(<StaticRouterProvider ... />):把静态路由器渲染为 HTML 字符串,包装为text/html响应返回。
提示:如果路由使用了
lazy路由模块,务必使用createStaticHandler解构出的dataRoutes传给createStaticRouter(如上例),而不是原始routes。因为 handler 内部已完成懒加载模块的解析与路由增强,dataRoutes才是与context匹配的对象。这一点在测试 data-static-router-test.tsx 的 "renders an initialized router with lazy routes" 用例中有完整验证。
Props 详解:四个参数的精确语义
context
文档说明:
The
StaticHandlerContextreturned fromStaticHandler'squery
即 query(request) 的返回值(非 Response 时)。它承载了渲染所需的全部数据:location、matches、loaderData、actionData、errors、basename 等。在 server.tsx 的源码中可以看到,context 同时被用作:
dataRouterContext.staticContext,注入DataRouterContext供路由内部读取;basename的来源:context.basename || "/"。
router
文档说明:
The static
DataRouterfromcreateStaticRouter
由 createStaticRouter 生成的静态路由器实例。从源码结构看,它本质上是一个只读的数据容器:
get state() {
return {
historyAction: NavigationType.Pop,
location: context.location,
matches,
loaderData: context.loaderData,
actionData: context.actionData,
errors: context.errors,
initialized: true,
navigation: IDLE_NAVIGATION,
fetchers: new Map(),
blockers: new Map(),
// ...
};
},
所有动态方法(navigate、fetch、revalidate、initialize、subscribe、dispose 等)都会抛出形如 You cannot use router.navigate() on the server because it is a stateless environment 的错误(见 server.tsx)。这与 <StaticRouterProvider> “不可导航” 的定位完全一致——服务端只有一次渲染机会,任何试图改变 URL 的调用都必须被显式禁止。
hydrate
文档说明:
Whether to hydrate the router on the client (default
true)
控制是否在 HTML 中输出水合脚本。默认值为 true。当设为 false 时,页面不包含任何内联 <script>,客户端不会执行水合(适用于预渲染纯静态页面或测试场景)。测试用例 data-static-router-test.tsx 验证了 hydrate={false} 时输出中不出现 <script>、window 或 __staticRouterHydrationData。
nonce
文档说明:
The
nonceto use for the hydration<script>tag
水合 <script> 标签使用的 CSP nonce。如果项目启用了 Content Security Policy,需要通过它放行内联脚本。对应测试 data-static-router-test.tsx 验证了输出形如 <script nonce="nonce-string">window.__staticRouterHydrationData = JSON.parse(...)</script>。
两个必传 props 缺少时,组件会抛出明确的错误:
You must provide `router` and `context` to <StaticRouterProvider>
该 invariant 断言位于 server.tsx,并有测试 data-static-router-test.tsx 对两种缺参场景分别做了快照验证。
渲染流程:从 props 到四层 Context
理解 StaticRouterProvider 的渲染结构,有助于排查 SSR 场景下的 Context 问题。源码(server.tsx)按以下层级组织输出:
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<FetchersContext.Provider value={fetchersContext}>
<ViewTransitionContext.Provider value={{ isTransitioning: false }}>
<Router ... static={true} useTransitions={false}>
<DataRoutes manifest={router.manifest} routes={router.routes}
future={router.future} state={state} isStatic={true} />
</Router>
</ViewTransitionContext.Provider>
</FetchersContext.Provider>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
几个值得注意的实现决策:
fetchersContext恒为空Map:服务端渲染不产生 fetcher 请求,getFetcher()也固定返回IDLE_FETCHER;ViewTransitionContext固定为{ isTransitioning: false }:静态渲染不存在视图过渡状态;Router传入static={true}与useTransitions={false}:内部组件据此走静态分支(如Link渲染为纯<a data-discover="true">,不注册点击拦截);basename取自context.basename:createStaticHandler(routes, { basename: "/base" })设置的 basename 会一路透传到渲染的<Link>上。测试 data-static-router-test.tsx 验证了/base/the/path请求下链接正确渲染为href="/base/the/other/path",而useLocation()看到的仍是去除 basename 后的/the/path。
此外,同一个文件里还有一个声明式模式的兄弟组件 StaticRouter(@mode declarative),它基于 location prop 构造无状态 navigator,服务于不使用数据路由的场景;<StaticRouterProvider> 则属于数据路由模式(@mode data),两者不要混淆。
Hydration 脚本:双重序列化与 HTML 转义
hydrate 为 true(默认)时,组件会把数据嵌入页面。源码(server.tsx)的关键片段:
if (hydrate !== false) {
let data = {
loaderData: context.loaderData,
actionData: context.actionData,
errors: serializeErrors(context.errors),
};
// Use JSON.parse here instead of embedding a raw JS object here to speed
// up parsing on the client. Dual-stringify is needed to ensure all quotes
// are properly escaped in the resulting string.
let json = escapeHtml(JSON.stringify(JSON.stringify(data)));
hydrateScript = `window.__staticRouterHydrationData = JSON.parse(${json});`;
}
这段实现回答了两个容易被忽略的问题:
为什么双重 JSON.stringify?
直接内联一个 JS 对象字面量需要逐字段手写转义逻辑,而把 JSON 字符串再次 stringify 成字符串字面量,可以交给 JSON 规范本身完成全部引号转义。客户端侧再执行一次 JSON.parse 即可还原对象。源码注释明确提到这样做是为了客户端解析更快(引用了 V8 团队关于 JSON 解析性能的分析)。测试 data-static-router-test.tsx 精确断言了输出格式:
<script>window.__staticRouterHydrationData = JSON.parse("{...}");</script>
为什么还要 escapeHtml?
loader 返回的数据里完全可能包含 </script> 这类破坏 HTML 结构的字符串。escapeHtml 将其转义为 \u003c 形式,防止内联脚本提前终止。测试用例 data-static-router-test.tsx 专门验证了 loader 返回 uh </script> oh 时,输出中被安全编码为 \u003c/script\u003e。
错误数据的安全序列化:serializeErrors
errors 字段不能直接 JSON 化——Error 实例的 message/stack 是自有属性结构复杂,RouteErrorResponse 更是自定义类实例。源码中的 serializeErrors 按类型分别处理:
| 错误类型 | 序列化结果 | 说明 |
|---|---|---|
RouteErrorResponse(如 throw new Response(..., { status: 404 })) |
{ ...val, __type: "RouteErrorResponse" } |
保留 status、statusText、data 等字段 |
普通 Error |
{ message: val.message, __type: "Error" } |
故意丢弃 stack,注释写明 "Do not serialize stack traces from SSR for security reasons" |
Error 子类(如 ReferenceError) |
额外附加 __subType: val.name |
客户端水合时可重建相同错误类型 |
| 其他值 | 原样透传 | 自定义错误对象等 |
三个对应的测试用例分别验证了上述形态(见 data-static-router-test.tsx):ErrorResponse 序列化为带 __type: "RouteErrorResponse" 的对象;new Error("oh no") 序列化为 { message: "oh no", __type: "Error" };new ReferenceError("oh no") 额外带上 __subType: "ReferenceError"。
源码注释还特别提示:修改此处的序列化逻辑时,需要同步修改客户端 lib/dom/lib.tsx 中的 deserializeErrors,二者是一对镜像逻辑,保证水合前后错误对象类型一致。
无状态导航器:为什么导航一定会报错
<StaticRouterProvider> 与 <StaticRouter> 共享同一个 getStatelessNavigator()(server.tsx)。它只提供 createHref 与 encodeLocation,而所有变更历史的方法都被替换为抛错:
push(to: To) {
throw new Error(
`You cannot use navigator.push() on the server because it is a stateless ` +
`environment. This error was probably triggered when you did a ` +
`\`navigate(${JSON.stringify(to)})\` somewhere in your app.`,
);
},
replace、go、back、forward 同理。注意错误信息里直接内嵌了触发导航的参数(JSON.stringify(to)),这在排查“loader 里意外调用 navigate”这类问题时非常有用——服务端渲染期间任何 navigate(...) 都会带着目标路径直接炸出。
encodeLocation 还有一个细节:对 href 结尾的空格做 %20 预编码(server.tsx),因为把 href 当作完整 URL 解析时尾部空格会被吃掉,而这些空格可能是祖先路由 splat 参数的合法部分。测试用例 data-static-router-test.tsx 验证了:路径 /path/with space 下自动生成的 <a href> 与 <form action> 会编码为 with%20space,而用户显式指定的 to 值则保持原样不编码,以此避免水合不一致。
完整实战示例
综合以上机制,一个可直接复用的 Express/Node 风格服务端入口如下(所有 API 均可在 packages/react-router/index.ts 的公共导出中找到,包括 StaticRouterProvider、createStaticRouter、createStaticHandler):
import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import {
createStaticHandler,
createStaticRouter,
StaticRouterProvider,
} from "react-router";
let staticHandler = createStaticHandler(routes);
export async function handleRequest(request: Request) {
let context = await staticHandler.query(request);
// 重定向或 loader 抛出的 Response 直接透传
if (context instanceof Response) {
return context;
}
let html = ReactDOMServer.renderToString(
<React.StrictMode>
<StaticRouterProvider
router={createStaticRouter(staticHandler.dataRoutes, context)}
context={context}
nonce={nonce} // 启用 CSP 时传入
hydrate={true} // 默认值,可省略
/>
</React.StrictMode>
);
return new Response(
`<!DOCTYPE html><html><body><div id="root">${html}</div></body></html>`,
{ headers: { "Content-Type": "text/html" } }
);
}
要点回顾:
createStaticHandler可以只创建一次并复用于每个请求(上例在模块顶层创建),文档示例中每次请求内联创建同样合法;query返回Response时必须短路返回,这是处理redirect的标准姿势;lazy路由场景必须传staticHandler.dataRoutes而非原始routes;- 客户端水合脚本写入
window.__staticRouterHydrationData,包含loaderData、actionData、errors(已按上文规则序列化),浏览器端水合时消费该数据,避免 loader 重复执行。
小结
<StaticRouterProvider> 是 react-router 数据路由模式下的 SSR 出口:它用 StaticHandlerContext + 静态 DataRouter 完成一次不可导航的渲染,通过四层 Context 组织输出,用双重 JSON.stringify + HTML 转义安全地输出 window.__staticRouterHydrationData 水合脚本,并用有状态的错误序列化(__type/__subType 标记、剔除服务端 stack)保证错误对象安全地跨边界传输。相关实现集中在 packages/react-router/lib/dom/server.tsx,行为边界由 packages/react-router/tests/dom/data-static-router-test.tsx 的十余个用例(hydration 脚本格式、转义、nonce、禁用 hydration、缺参报错、lazy 路由、basename、边界追踪等)完整覆盖,可作为自研 SSR 适配层时的行为参照。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00