首页
/ react-router StaticRouterProvider 深度解析:静态数据路由器的 SSR 渲染与 Hydration 数据原理

react-router StaticRouterProvider 深度解析:静态数据路由器的 SSR 渲染与 Hydration 数据原理

2026-09-06 23:48:14作者:温艾琴Wonderful

在 React 应用中做服务端渲染(SSR)时,<StaticRouterProvider> 是 react-router 数据路由(Data Router)模式下打通“服务器 → 浏览器”这条链路的核心组件。它接收由 createStaticHandler 发起数据请求后得到的 StaticHandlerContext 与静态 DataRouter,在服务端一次性渲染出完整的 HTML,并把 loader/action 数据以脚本形式嵌入页面,供客户端水合时直接复用。读完本文,你将掌握 StaticRouterProvider 的完整用法、四个 props 的精确语义、Hydration 脚本的双重序列化与安全转义机制,以及它在无状态导航环境下的实现细节。

定位:一个“不能导航”的 DataRouter

官方文档(StaticRouterProvider.md)对它的定义非常简洁:

A DataRouter that may not navigate to any other Location. 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 请求处理流水线,值得逐行理解:

  1. createStaticHandler(routes):创建静态处理器,返回 { query, queryRoute, dataRoutes }。其中 query() 面向文档请求(document requests),queryRoute() 面向定向路由请求,dataRoutes 是内部转换后的数据路由对象(签名与参数详见 createStaticHandler.md,实现位于 router.ts)。
  2. await query(request):执行 loader/action 数据请求。返回值有两种可能:
    • StaticHandlerContext:正常渲染所需的数据上下文;
    • Response 实例:通常是重定向或 loader 抛出的 Response(例如 throw redirect("/login"))。此时应直接把该 Response 作为当前请求的返回值,不再渲染。
  3. createStaticRouter(dataRoutes, context):用 query 的返回上下文构造静态路由器(签名见 createStaticRouter.md)。
  4. 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 StaticHandlerContext returned from StaticHandler's query

query(request) 的返回值(非 Response 时)。它承载了渲染所需的全部数据:locationmatchesloaderDataactionDataerrorsbasename 等。在 server.tsx 的源码中可以看到,context 同时被用作:

  • dataRouterContext.staticContext,注入 DataRouterContext 供路由内部读取;
  • basename 的来源:context.basename || "/"

router

文档说明:

The static DataRouter from createStaticRouter

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(),
    // ...
  };
},

所有动态方法(navigatefetchrevalidateinitializesubscribedispose 等)都会抛出形如 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 nonce to 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.basenamecreateStaticHandler(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 转义

hydratetrue(默认)时,组件会把数据嵌入页面。源码(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" } 保留 statusstatusTextdata 等字段
普通 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)。它只提供 createHrefencodeLocation,而所有变更历史的方法都被替换为抛错:

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.`,
  );
},

replacegobackforward 同理。注意错误信息里直接内嵌了触发导航的参数(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 的公共导出中找到,包括 StaticRouterProvidercreateStaticRoutercreateStaticHandler):

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,包含 loaderDataactionDataerrors(已按上文规则序列化),浏览器端水合时消费该数据,避免 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 适配层时的行为参照。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391