react-router createBrowserRouter 深度指南:Data Router 创建、Hydration 数据注入与「战争迷雾」式动态路由发现
在 React Router 中,createBrowserRouter 是构建 Data Mode 应用最核心的入口函数:它基于浏览器 History API 创建一个管理应用路径的 data router,并将路由、loader/action、中间件、hydration 数据等全部串联起来。本文基于仓库官方文档 createBrowserRouter 与对应源码实现,完整覆盖其函数签名、全部 8 个配置选项(含 patchRoutesOnNavigation 的动态路由发现机制)的用法,并结合源码剖析 router 的初始化链路,读完即可独立完成一个支持服务端渲染水合、动态路由树与可观测性埋点的 Data Mode 应用。
一、createBrowserRouter 的定位:创建 Data Router
文档对该函数的定义是:创建一个通过 history.pushState 和 history.replaceState 管理应用路径的 data router。与声明式路由(<BrowserRouter> + <Routes>)不同,data router 是一个持有完整内部状态的运行时对象——它自己负责执行 loader/action、管理导航状态(loading/submitting)、处理 fetcher 请求,然后把这些状态通过 <RouterProvider> 暴露给 React 组件树。
由此产生一条关键使用约束(文档原文即如此强调):
Data Router 不应存放在 React 状态中。 你应该在 React 组件树之外只创建一次 router,然后把它传给
<RouterProvider>。
也就是说,router 的创建必须发生在 React 生命周期之外(通常在入口文件的模块顶层),这样导航状态才不会与组件的重渲染产生耦合。
函数签名
function createBrowserRouter(
routes: RouteObject[],
opts?: DOMRouterOpts,
): DataRouter
routes: RouteObject[]—— 应用路由树(路由对象数组,详见 route-object 文档);opts?: DOMRouterOpts—— DOM 环境专属配置项,下节逐项展开;- 返回一个已初始化(initialized)的 data router,供
<RouterProvider>消费。
二、DOMRouterOpts 配置项全景
DOMRouterOpts 接口定义在 packages/react-router/lib/dom/lib.tsx,完整包含 8 个可选字段:
| 配置项 | 类型 | 作用 |
|---|---|---|
basename |
string |
应用挂载的基础路径前缀 |
getContext |
() => RouterContextProvider |
为客户端 action/loader/中间件提供 context,每次导航或 fetcher 调用都会重新生成一个实例 |
future |
Partial<FutureConfig> |
开启 router 的未来特性开关 |
hydrationData |
HydrationState |
服务端渲染场景下手动注入 hydration 数据(loaderData/errors/actionData) |
instrumentations |
ClientInstrumentation[] |
路由与路由级(loader/action/middleware)的可观测性埋点 |
dataStrategy |
DataStrategyFunction |
覆盖默认的「并行执行 loader」数据加载策略 |
patchRoutesOnNavigation |
PatchRoutesOnNavigationFunction |
在导航发生无法匹配时动态补全路由树(「战争迷雾」) |
window |
Window |
覆盖全局 window 对象,默认为全局实例 |
从源码实现看,这些选项会被逐项透传给底层的通用 createRouter,见 lib.tsx 中的 createBrowserRouter 实现:
export function createBrowserRouter(
routes: RouteObject[],
opts?: DOMRouterOpts,
): DataRouter {
return createRouter({
basename: opts?.basename,
getContext: opts?.getContext,
future: opts?.future,
history: createBrowserHistory({ window: opts?.window }),
hydrationData: opts?.hydrationData || parseHydrationData(),
routes,
mapRouteProperties: defaultMapRouteProperties,
hydrationRouteProperties,
dataStrategy: opts?.dataStrategy,
patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,
window: opts?.window,
instrumentations: opts?.instrumentations,
}).initialize();
}
这段实现透露了几个源码级事实:
createBrowserRouter本质上是对createRouter的薄封装(createRouter定义于 packages/react-router/lib/router/router.ts),它注入的是 browser 专属的history实现,最后立即调用.initialize()完成初始化并执行首次数据加载。同文件中的createHashRouter(L689-L707)结构完全一致,只是把 history 换成了 hash 版。window选项被复用两次:既传入createBrowserHistory({ window })用于读取初始位置,又传给 router 本体用于绑定 popstate 等事件。hydrationData有隐式兜底:opts?.hydrationData || parseHydrationData()——当你不显式传入时,会回退到读取window.__staticRouterHydrationData全局变量(这是 Framework Mode 自动水合的通道)。
opts.basename:应用基础路径
用于应用部署在域名下的子路径时,例如 basename: "/app" 后所有路由都在 /app 前缀下匹配。该值直接透传给 createRouter 参与路由匹配与链接生成。
opts.future:特性开关
Partial<FutureConfig> 类型,用于提前启用尚在演进中的 router 行为,保证未来版本默认切换时应用不报错。
三、opts.dataStrategy:自定义数据加载策略
默认情况下 data router 会并行执行所有匹配的 loader。dataStrategy 允许你完全接管「先加载谁、如何加载、何时执行客户端中间件」的编排,官方详细指南见 data-strategy。
文档给出的标准示例展示了策略函数的入参(matches、request、runClientMiddleware)与返回值(以 routeId 为键的结果映射):
let router = createBrowserRouter(routes, {
async dataStrategy({
matches,
request,
runClientMiddleware,
}) {
const matchesToLoad = matches.filter((m) =>
m.shouldCallHandler(),
);
const results: Record<string, DataStrategyResult> = {};
await runClientMiddleware(() =>
Promise.all(
matchesToLoad.map(async (match) => {
results[match.route.id] = await match.resolve();
}),
),
);
return results;
},
});
要点:先用 shouldCallHandler() 过滤出真正需要执行 handler 的 match,再把 match.resolve() 调用包进 runClientMiddleware——后者确保路由级 middleware 在 loader 执行前跑完。这个示例实际上保留了默认行为(Promise.all 并行),你可以在此基础上改造成串行、按优先级排队或带重试的自定义编排。
四、opts.getContext:为 loader/action/中间件注入客户端 Context
getContext 返回一个 RouterContextProvider 实例,作为 context 参数传给客户端的 action、loader 与 middleware。注意其调用时机:每次导航或 fetcher 调用都会生成一个全新的 context 实例。
文档示例演示了如何把 API 客户端注入到路由数据层:
import {
createContext,
RouterContextProvider,
} from "react-router";
const apiClientContext = createContext<APIClient>();
function createBrowserRouter(routes, {
getContext() {
let context = new RouterContextProvider();
context.set(apiClientContext, getApiClient());
return context;
}
})
这与 React 的 createContext 配合使用:在 router 内部(loader/action 无法访问 React 树)拿到的 context,与组件中通过 apiClientContext 访问的是同一套语义化的依赖注入通道。
五、opts.hydrationData:服务端渲染数据的水合注入
当采用 SSR 且选择手动水合(opt-out of automatic hydration)时,hydrationData 让你把服务端渲染阶段拿到的数据传回客户端,避免 loader 二次请求。这份数据几乎总是 StaticHandler 的 query 方法 返回的 StaticHandlerContext 值的一个子集:
const router = createBrowserRouter(routes, {
hydrationData: {
loaderData: {
// [routeId]: serverLoaderData
},
// may also include `errors` and/or `actionData`
},
});
部分水合数据(Partial Hydration Data)
多数场景你会传入完整的 loaderData,但进阶用法(例如 Framework Mode 的 clientLoader)可能只想为部分路由(如应用外壳/布局)提供 loader 数据,让其余路由在 hydration 期间显示 HydrateFallback 骨架并执行自己的 loader。
文档明确了 loader 在 hydration 期间会执行的两个条件:
- 没有提供 hydration 数据 —— 此时初始水合会渲染
HydrateFallback组件; loader.hydrate属性被设为true—— 即使你已经在初次水合时渲染了内容(没有 fallback),loader 仍会在 hydration 时再跑一次(典型用途是用 hydration 数据预热缓存)。
const router = createBrowserRouter(
[
{
id: "root",
loader: rootLoader,
Component: Root,
children: [
{
id: "index",
loader: indexLoader,
HydrateFallback: IndexSkeleton,
Component: Index,
},
],
},
],
{
hydrationData: {
loaderData: {
root: "ROOT DATA",
// No index data provided
},
},
}
);
在这个例子中 root 直接用服务端数据水合,而 index 因缺少数据先渲染 IndexSkeleton,同时执行 indexLoader。仓库的单元测试 data-browser-router-test.tsx 对此有大量覆盖,包括「首次数据获取期间渲染 HydrateFallback」「dataStrategy 返回部分结果时清除 HydrateFallback」「hydration 中间件执行期间渲染祖先/自身 HydrateFallback」等用例,验证了上述行为的真实语义。
一个容易被忽略的源码细节:当你不传 hydrationData 时,parseHydrationData() 会自动读取 window.__staticRouterHydrationData;若其中包含 errors,还会经过 deserializeErrors 将序列化的错误对象重建为真实的 RouteErrorResponse 或对应子类型的 Error(如 ReferenceError),但会清空客户端 stack trace——源码注释说明这是出于安全原因不序列化 SSR 堆栈。
六、opts.instrumentations:router 与路由级可观测性埋点
instrumentations 是一个埋点对象数组,允许你在 router 初始化之前对 router 整体和各个路由(包括后续通过 route.lazy 或 patchRoutesOnNavigation 动态加入的路由)进行插桩,典型用途是把导航、fetch、loader/action/middleware 包上日志与性能追踪。详见 instrumentation 指南。
文档给出的完整示例演示了两个挂载点——router 级(navigate/fetch)与 route 级(middleware/loader/action),每个插桩点都收到 (impl, info),你只需包裹 impl 调用即可记录耗时:
let router = createBrowserRouter(routes, {
instrumentations: [logging]
});
let logging = {
router({ instrument }) {
instrument({
navigate: (impl, info) => logExecution(`navigate ${info.to}`, impl),
fetch: (impl, info) => logExecution(`fetch ${info.to}`, impl)
});
},
route({ instrument, id }) {
instrument({
middleware: (impl, info) => logExecution(
`middleware ${info.request.url} (route ${id})`,
impl
),
loader: (impl, info) => logExecution(
`loader ${info.request.url} (route ${id})`,
impl
),
action: (impl, info) => logExecution(
`action ${info.request.url} (route ${id})`,
impl
),
})
}
};
async function logExecution(label: string, impl: () => Promise<void>) {
let start = performance.now();
console.log(`start ${label}`);
await impl();
let duration = Math.round(performance.now() - start);
console.log(`end ${label} (${duration}ms)`);
}
这套装饰器模式的价值在于:埋点逻辑与业务路由代码完全解耦,且对动态补入的路由同样生效(埋点在路由加入树时即被应用)。
七、opts.patchRoutesOnNavigation:「战争迷雾」式的动态路由发现
这是 createBrowserRouter 最有分量的高级选项。理解它需要先理解默认设计的取舍:
- 默认模式:
createBrowserRouter(routes)要求你一次性提供完整路由树,好处是 React Router 可以做同步路由匹配、执行 loader、以最乐观方式渲染组件,避免任何数据瀑布;代价是初始 JS bundle 天然更大,应用增长后会拖慢启动。 route.lazy(v6.9.0 引入):折中方案——路由定义(path、index等轻量部分)仍然前置提供,但路由实现(loader、Component等重负载部分)延迟到真正导航到时才加载。路由匹配仍是同步的。patchRoutesOnNavigation:当应用巨大到「前置提供所有路由定义」本身都不可承受时(某些 Micro-Frontend / Module-Federation 架构下甚至做不到),就用它在运行时按需「发现」路由树的局部。这个特性常被称为 "Fog of War"(战争迷雾)——如同游戏中世界随角色移动而展开,路由树也随用户导航而扩展,最终只加载用户真正访问过的部分。
触发时机与执行阶段
patchRoutesOnNavigation 会在 React Router 无法匹配某个 path 时被调用,参数包括 path、部分 matches,以及一个 patch 函数(把新路由打到树的指定位置)。执行阶段:
GET请求:在导航的loading阶段执行;- 非
GET请求:在导航的submitting阶段执行。
用例 1:向已有路由打补丁注入子路由
const router = createBrowserRouter(
[
{
id: "root",
path: "/",
Component: RootComponent,
},
],
{
async patchRoutesOnNavigation({ patch, path }) {
if (path === "/a") {
// Load/patch the `a` route as a child of the route with id `root`
let route = await getARoute();
// ^ { path: 'a', Component: A }
patch("root", [route]);
}
},
}
);
用户点击 /a 链接时,初次匹配失败,patchRoutesOnNavigation 收到 path = "/a" 与包含 root 匹配的 matches 数组;调用 patch('root', [route]) 后新路由挂到 root 之下,React Router 用更新后的树重新匹配,成功命中 /a,导航完成。
用例 2:补丁新的根级路由
若新路由没有父级,把 routeId 传 null:
const router = createBrowserRouter(
[
{
id: "root",
path: "/",
Component: RootComponent,
},
],
{
async patchRoutesOnNavigation({ patch, path }) {
if (path === "/root-sibling") {
// Load/patch the `/root-sibling` route as a sibling of the root route
let route = await getRootSiblingRoute();
// ^ { path: '/root-sibling', Component: RootSibling }
patch(null, [route]);
}
},
}
);
用例 3:异步拉取整棵子树
可以按路径前缀懒加载应用的不同板块:
let router = createBrowserRouter(
[
{
path: "/",
Component: Home,
},
],
{
async patchRoutesOnNavigation({ patch, path }) {
if (path.startsWith("/dashboard")) {
let children = await import("./dashboard");
patch(null, children);
}
if (path.startsWith("/account")) {
let children = await import("./account");
patch(null, children);
}
},
}
);
一个重要的执行语义(来自文档内嵌提示):若正在执行的 patchRoutesOnNavigation 被后续导航打断,被中断执行中剩余的 patch 调用不会更新路由树,因为该操作已被取消。 这意味着你可以在回调里放心执行 await import(...),不会污染已被放弃的导航。
用例 4:把路由发现与路由定义同置
不想自己写伪匹配逻辑时,可以借助部分 matches 数组和路由上的 handle 字段,让子路由定义与父路由定义保持同置:
let router = createBrowserRouter(
[
{
path: "/",
Component: Home,
},
{
path: "/dashboard",
children: [
{
// If we want to include /dashboard in the critical routes, we need to
// also include it's index route since patchRoutesOnNavigation will not be
// called on a navigation to `/dashboard` because it will have successfully
// matched the `/dashboard` parent route
index: true,
// ...
},
],
handle: {
lazyChildren: () => import("./dashboard"),
},
},
{
path: "/account",
children: [
{
index: true,
// ...
},
],
handle: {
lazyChildren: () => import("./account"),
},
},
],
{
async patchRoutesOnNavigation({ matches, patch }) {
let leafRoute = matches[matches.length - 1]?.route;
if (leafRoute?.handle?.lazyChildren) {
let children =
await leafRoute.handle.lazyChildren();
patch(leafRoute.id, children);
}
},
}
);
注意示例中的注释陷阱:如果希望 /dashboard 本体属于关键路由,必须同时提供它的 index 子路由——因为导航到 /dashboard 会成功匹配父路由,patchRoutesOnNavigation 根本不会被调用,index 若缺省会直接 404。
含参路由的匹配歧义(重要设计细节)
这是该机制中最容易被误用的一点。React Router 使用带权重的路由排序为给定路径找最佳匹配,而部分已知的路由树会引入歧义:
- 若匹配到的是完全静态的路由(如
path: "/about/contact-us"),它由纯静态 URL 段构成,可以确定这就是正确匹配,无需再询问是否存在更高分路由; - 但含动态参数或 splat 的路由不能做此假设——可能还存在一个尚未发现但得分更高的路由。
文档的完整示例:假设应用真实路由树是「首页 + /blog(含 new 与 :slug 两个子路由)」,而启动时只注册了首页:
// Assume this is the full route tree for your app
const routes = [
{
path: "/",
Component: Home,
},
{
id: "blog",
path: "/blog",
Component: BlogLayout,
children: [
{ path: "new", Component: NewPost },
{ path: ":slug", Component: BlogPost },
],
},
];
// Start with only the index route
const router = createBrowserRouter(
[
{
path: "/",
Component: Home,
},
],
{
async patchRoutesOnNavigation({ patch, path }) {
if (path === "/blog/new") {
patch("blog", [
{
path: "new",
Component: NewPost,
},
]);
} else if (path.startsWith("/blog")) {
patch("blog", [
{
path: ":slug",
Component: BlogPost,
},
]);
}
},
}
);
问题推演:用户先看某篇博客(/blog/my-post),:slug 被打入路由树;随后用户访问 /blog/new 写新文章——此时会先匹配上 /blog/:slug,但这不是正确匹配!因此规则是:只要匹配到的路径中包含至少一个参数,React Router 就会额外调用一次 patchRoutesOnNavigation 并重新匹配,确认没有更高分的未发现路由。
由此引出的性能建议:如果你的 patchRoutesOnNavigation 实现代价高昂,或会向后端发起副作用 fetch,应维护一个「已发现路径」缓存,避免重复拉取:
let discoveredRoutes = new Set();
const router = createBrowserRouter(routes, {
async patchRoutesOnNavigation({ patch, path }) {
if (discoveredRoutes.has(path)) {
// We've seen this before so nothing to patch in and we can let the router
// use the routes it already knows about
return;
}
discoveredRoutes.add(path);
// ... patch routes in accordingly
},
});
八、opts.window:window 对象覆盖
最后一个选项最为简单:覆盖默认的 window 对象引用,默认为全局 window 实例。在 createBrowserRouter 源码 中它被传递了两处——createBrowserHistory({ window }) 与 router 初始化参数,分别影响初始位置解析与浏览器事件监听绑定,可用于测试隔离或特殊环境下的多窗口场景。
九、返回值与配套 API
createBrowserRouter 返回一个已初始化的 data router,其职责链在源码中一目了然:createRouter(router.ts)→ 注入 createBrowserHistory(基于 getUrlBasedHistory,从 history.state 中解析 masked location 以支持链接伪装)→ .initialize() 触发首次匹配与数据加载。
与它构成家族关系的其他 data router 创建函数(文档索引见 data-routers):
createHashRouter:同样接受DOMRouterOpts,区别仅在于用 URL hash 而非标准 URL 存储位置,适合无法控制服务端的路由 fallback 配置;createMemoryRouter:基于内存 location,主要用于测试;createStaticRouter/createStaticHandler:服务端场景,与本文hydrationData的StaticHandler.query返回值直接对应。
最终这些 router 都交由 <RouterProvider> 挂载到 React 树上。
十、小结与实践建议
- 创建时机:router 在 React 树外只创建一次,绝不放入 state;
- SSR 应用:优先显式传入
hydrationData(含loaderData/errors/actionData),部分水合场景利用「无 hydration 数据」或loader.hydrate: true两个触发条件精细控制骨架屏与缓存预热; - 大型应用:优先
route.lazy拆实现、保留路由定义前置;只有在路由定义本身无法前置(微前端等)时才升级到patchRoutesOnNavigation,并务必为含参路由路径做好重复匹配的准备与discoveredRoutes式缓存; - 可观测性:用
instrumentations统一包裹 navigate/fetch 与 loader/action/middleware,避免在各处散落埋点代码; - 策略编排:需要串行、限流、重试或自定义中间件执行时机时,用
dataStrategy接管 loader 执行编排。
以上行为均可在仓库中直接验证:API 文档 docs/api/data-routers/createBrowserRouter.md、实现源码 packages/react-router/lib/dom/lib.tsx、hydration 与 HydrateFallback 的单元测试 packages/react-router/tests/dom/data-browser-router-test.tsx,以及 fog-of-war 相关的集成测试 integration/fog-of-war-test.ts。
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 StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00