React Router Server Bundles 完全指南:利用 serverBundles 将路由树拆分为多个服务端请求处理入口
【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router
Server Bundles(服务端 Bundle 拆分)是 React Router 为**托管方集成(hosting provider integrations)**设计的进阶特性,属于 [MODES: framework] 场景。本文以官方文档 docs/how-to/server-bundles.md 为主体,结合仓库源码与集成测试,系统讲解如何在 react-router.config.ts 中通过serverBundles函数把整棵路由树按需分派到不同的服务端 bundle,每个 bundle 各自导出独立的路由子集请求处理器,并在buildEnd钩子中借助 Build Manifest 驱动自定义路由分发层。读完本文,你将能设计"认证区独立部署 / 大型站点按分区拆分"的服务端产物结构,并理解其底层约束(可寻址路由、bundle ID 命名规则、SPA 与 RSC 限制等)。
1. 特性背景:默认单 bundle 与多 bundle 的取舍
在默认情况下,React Router(@react-router/dev的 Vite 插件)会把整个应用的服务端代码编译为单个 server bundle,该 bundle 导出一个覆盖全部路由的请求处理器(request handler)函数,产物通常位于build/server/index.js(默认值见 config/config.ts:buildDirectory: "build"、serverBuildFile: "index.js")。
然而在某些托管/部署场景下,你会希望:
- 将应用的路由树拆成多个服务端 bundle,每个 bundle 只为其中的一部分路由提供服务端请求处理;
- 由部署平台上层的自定义路由层(edge gateway、CDN、负载均衡等)根据 URL 把请求分发给正确的 bundle 入口。
这好比把"一整台应用服务器"变成"一组按 URL 分区负责的服务器组",各分区可以独立扩缩容、独立回滚、独立冷启动。React Router 通过react-router.config.ts中的serverBundles配置项提供这一灵活性。
需要特别强调的是,这是文档明确标注的高级特性:一旦拆分为多 bundle,你的应用前方就必须存在一个自定义路由层,负责把请求导向正确的 bundle,React Router 本身不会替你完成这一分发。集成测试 integration/vite-server-bundles-test.ts 也印证了这一点——它逐个启动"只承载某一个 bundle"的服务进程来验证行为,而不是依赖单个进程同时服务所有路由。
2.serverBundles函数:路由 → Bundle 的分派器
serverBundles是 react-router.config.ts 顶层配置之一。其类型定义于 config/config.ts:
export type ServerBundlesFunction = (args: { branch: BranchRoute[]; }) => string | Promise<string>;- 它会被**路由树中的每条可寻址路由(addressable route)**调用;
- 返回一个字符串作为该路由所属的server bundle ID;
- 这些 bundle ID 会被用作服务端构建输出目录的名称(例如返回
"authenticated",则产物出现在build/server/authenticated/)。
2.1 官方示例:按布局路由拆分认证区
文档给出的典型用法是:为某个布局下的所有路由单独创建 bundle。由于函数收到的是从根路由到当前路由的整条branch,你可以通过检查祖先路由的id来判断归属:
import type { Config } from "@react-router/dev/config"; export default { // ... serverBundles: ({ branch }) => { const isAuthenticatedRoute = branch.some((route) => route.id.split("/").includes("_authenticated"), ); return isAuthenticatedRoute ? "authenticated" : "unauthenticated"; }, } satisfies Config;该示例中,凡是被_authenticated布局(以点号分段标识嵌套关系,其 routeid形如routes/_authenticated)包裹的子路由,其 branch 里必然包含该布局路由,因此会被分派到authenticatedbundle;其余路由进入unauthenticatedbundle。
2.2 底层调用机制:getBuildManifest
真正执行分派的逻辑位于 vite/plugin.ts 的getBuildManifest:
- 取出
reactRouterConfig中的{ routes, serverBundles, appDirectory }; - 若未配置
serverBundles,直接返回{ routes }(即单 bundle 形态); - 否则,遍历
getAddressableRoutes(routes)(见下文"可寻址路由"),对每条路由用getRouteBranch向上回溯父级得到branch; - 为每条路由调用
serverBundles({ branch }),把返回的 ID 记录进routeIdToServerBundleId; - 将同一 ID 的入口文件路径登记进
serverBundles映射。
其中有两个细节值得注意:
- 传给函数的
route.file是绝对路径——源码注释明确写着 "Ensure absolute paths are passed to the serverBundles function"(见 plugin.ts),便于你在函数内部直接fs.readFile(route.file)读取该路由的源文件做进一步处理(集成测试正是这么做的,见 vite-server-bundles-test.ts); - 该函数支持异步:类型为
string | Promise<string>,源码中await serverBundles({ branch })也验证了这一点。
3. branch 中每个 route 的属性
serverBundles函数收到的branch是从根路由到当前路由的完整祖先链数组(源码getRouteBranch通过逐级沿parentId回溯再反转得到,见 plugin.ts)。为了隔离实现细节,框架只暴露了 4 个属性的子集,定义见 config/config.ts:
| 属性 | 类型 | 说明 |
|---|---|---|
id | string | 路由唯一 ID,命名与其file一致:相对 app 目录、去掉扩展名。例如app/routes/gists.$username.tsx的 id 为routes/gists.$username |
path | string | 该路由用于匹配 URL pathname 的路径片段 |
file | string | 该路由入口文件路径(调用时被加工为以项目根为基准的绝对路径) |
index | boolean | 该路由是否为 index 路由(匹配父路径本身) |
其中BranchRoute类型正是通过Pick<RouteManifestEntry, "id" | "path" | "file" | "index">实现"只暴露子集"的(见 config/config.ts),这也是为什么你在配置里拿不到parentId、loader等其余字段。
3.1 可寻址路由:哪些路由会触发 serverBundles
文档明确指出:serverBundles函数不会为不可寻址的路由(如 pathless 布局路由)单独调用。源码 plugin.ts 的getAddressableRoutes说明了取舍逻辑:
- index 路由的父路由会被跳过——因为 index 路由"接管"了父路由的路径,父路由本身没有可寻址的 URL;
- 没有
path且不是 index 的路由(即 pathless 布局)会被跳过——它们只能经由后代路由被访问。
也就是说,真正的分派判断发生在"每一条最终拥有独立 URL 的路由上",而branch中仍然会携带祖先的 pathless 布局,供你在判断逻辑里参考。
4. Bundle ID 的硬性约束与产物目录
serverBundleId并非任意字符串,源码对其有两条校验规则(见 plugin.ts):
- 返回值必须是
string,否则抛出The "serverBundles" function must return a string; - 必须匹配正则
/^[a-zA-Z0-9_]+$/——仅允许字母、数字和下划线,不允许连字符-。源码注释解释了原因:Server bundle IDs must be valid Vite environment names, so hyphens are not allowed。也就是说这些 ID 会作为 Vite Environment(服务端构建环境)的名称使用,因此必须遵循环境命名规则。
产物结构上,文档与测试共同确认:bundle ID 会成为服务端构建目录下的子目录名,每个 bundle 的入口文件位于build/server/<bundleId>/index.js。集成测试 vite-server-bundles-test.ts 对 manifest 的断言给出了真实输出示例:
{ "serverBundles": { "bundle_c": { "id": "bundle_c", "file": "build/server/bundle_c/index.js" }, "bundle_a": { "id": "bundle_a", "file": "build/server/bundle_a/index.js" }, "bundle_b": { "id": "bundle_b", "file": "build/server/bundle_b/index.js" }, "root": { "id": "root", "file": "build/server/root/index.js" } } }注意:文档提醒 Server Bundles 属于 framework 模式(SSR)下的能力。从源码看,若关闭 SSR(ssr: false),serverBundles会被置空忽略(config/config.ts);同时,当启用 RSC(React Server Components)时配置校验会直接报错,vite/rsc/plugin.ts 会把"serverBundles"加入错误清单——即Server Bundles 与 RSC 目前互斥。此外,开发模式下不会为各 bundle 分别构建,集成测试注释也说明"dev 模式下没有 server bundles,只是验证所有 bundle 的路由都可用"(见 vite-server-bundles-test.ts)。
5. 构建清单 Build Manifest 与 buildEnd 钩子
构建完成后,React Router 会调用配置中的buildEnd钩子,并传入一个buildManifest对象,供你在构建阶段把路由分发信息落盘或推送给上层路由系统。文档示例:
import type { Config } from "@react-router/dev/config"; export default { // ... buildEnd: async ({ buildManifest }) => { // ... }, } satisfies Config;buildEnd的完整签名(含reactRouterConfig与viteConfig)定义在 config/config.ts。当启用了 server bundles 时,buildManifest会由ServerBundlesBuildManifest形态组成(见 config/config.ts),包含三个部分:
5.1serverBundles:Bundle ID → 入口文件
{ [serverBundleId: string]: { id: string; file: string } },映射每个 bundle ID 到它的唯一标识与产物入口文件(相对项目根),即上文展示的build/server/<bundleId>/index.js。可用于枚举部署产物清单。
5.2routeIdToServerBundleId:路由 → Bundle 反向索引
Record<string, string>,把每个可寻址路由的id映射到其所属 bundle ID。示例见 vite-server-bundles-test.ts:
{ "routeIdToServerBundleId": { "routes/_index": "root", "routes/bundle_a": "bundle_a", "routes/bundle_a._index": "bundle_a", "routes/bundle_a.route_a": "bundle_a", "routes/_pathless.bundle_c.route_a": "bundle_c" } }注意路径型路由(如routes/_index)与嵌套路由(含 pathless 祖先)都出现在映射中,方便你按"URL 最终归属"直接查表。
5.3routes:完整路由清单
一个将 route ID 映射到路由元数据的清单(RouteManifest),用于驱动上层自定义路由层决定"这个 URL 该打到哪个 bundle"。集成测试里的每条路由元数据形如(见 vite-server-bundles-test.ts):
{ "routes/bundle_a.route_a": { "file": "app/routes/bundle_a.route_a.tsx", "id": "routes/bundle_a.route_a", "path": "route_a", "parentId": "routes/bundle_a" }, "routes/bundle_a._index": { "file": "app/routes/bundle_a._index.tsx", "id": "routes/bundle_a._index", "index": true, "parentId": "routes/bundle_a" } }字段通常包含file、id、path、parentId,index 路由额外带有index: true,根路由为path: ""、挂载在root下。这些元数据足以支撑一套"URL 前缀 → 路由匹配 → bundle 入口"的分发逻辑。
在实际项目中,你可以在buildEnd里把 manifest 序列化写入构建产物目录,供运行时或部署管道消费——集成测试正是这样做的:fs.promises.writeFile("build/test-manifest.json", JSON.stringify(buildManifest, null, 2))(见 vite-server-bundles-test.ts)。
6. 自定义路由层的搭建思路与验证
由于 React Router 官方构建产物只提供"每个 bundle 各导出自己路由子集的 handler",URL 级别的分发完全交给你的上层路由层。设计上通常遵循两条原则:
- 每个 bundle 的 handler 只响应自己名下的路由。集成测试通过
@react-router/serve逐 bundle 启动产物(入口路径形如build/server/<bundleId>/index.js,见 integration/helpers/vite.ts)验证:访问本 bundle 路由正常渲染,访问其他 bundle 的路由则得到 404(见 vite-server-bundles-test.ts)。这意味着你的路由层可以把"404 即非我负责"当作向后回退的信号。 - 利用 buildManifest 做确定性分发。结合
routes的路由元数据与routeIdToServerBundleId,可以精确构造"URL → 命中路由 → bundle 入口"的映射表,避免逐 bundle 试错。
从源码结构看,React Router 内置的 preview 服务器在遇到serverBundles时,会加载全部 bundle 的 handler,并按"匹配深度最深优先"排序后逐个尝试,直到拿到非 404 响应为止(见 vite/plugin.ts)。这段实现可视为"多 bundle 分发器"的参考范式——它特别处理了非 index bundle 命中/时只渲染根路由的边界情况。你可以借鉴同一思路在自己的网关/入口服务里实现分发,也可以选择完全基于 manifest 的确定性查表方案。
7. 关键结论与适用前提
- 适用模式:仅 framework(SSR)模式;
ssr: false时配置会被忽略,启用 RSC 时构建报错。 - 配置位置:react-router.config.ts 顶层的
serverBundles函数(类型定义见 config/config.ts)。 - 触发粒度:仅"可寻址路由"会触发分派;pathless 布局与 index 父路由不单独触发,但会出现在
branch中。 - 返回值:
string | Promise<string>;仅限字母、数字、下划线(Vite environment 名称约束);每个返回值对应build/server/<bundleId>/index.js一个独立入口。 - 消费方式:通过
buildEnd钩子读取buildManifest,利用serverBundles、routeIdToServerBundleId、routes三张表驱动自定义路由分发层。 - 分发责任:React Router 负责"按你的规则拆产物 + 暴露清单",URL 级路由分发由宿主平台的前置路由层负责。
如果你正在做边缘平台(Edge/FaaS)集成,或需要让不同分区应用独立部署、独立扩缩容,Server Bundles 配合buildEnd中的 manifest 即可构成一套自洽的"多入口 + 分发元数据"方案;单 server 部署的中小型应用则完全不需要启用它。更多相关背景可参阅 如何选择 SPA / Framework 模式 与 react-router.config.ts 参考。
【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考