news 2026/9/6 16:07:53

React Router 6.4 架构决策:如何把 Remix 分层到 React Router 6.4 之上

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Router 6.4 架构决策:如何把 Remix 分层到 React Router 6.4 之上

React Router 6.4 架构决策:如何把 Remix 分层到 React Router 6.4 之上

【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router

本篇基于 React Router 仓库中的架构决策记录 0007-remix-on-react-router-6-4-0.md,完整还原 2022 年 8 月团队在react-router@6.4.0发布前夕做出的关键工程决策:如何以"绞杀者模式"(strangler pattern)把 Remix 框架的 Data API 层逐步替换为 React Router 6.4 新引入的createStaticHandler等能力。读完你会掌握:迁移问题的功能拆解方法(服务端数据加载 / 服务端渲染 / 客户端水合 / 客户端数据加载四个切面)、feature-flag 双跑断言的灰度迁移手法,以及该决策在当前仓库源码中的最终落地形态。

一、背景:为什么要在 6.4.0 之后动 Remix 的地基

该决策记录(Date: 2022-08-16,Status: accepted)的起点是 0005-remixing-react-router.md 中提出的"Remixing React Router"计划——把 Remix 的 Data API(路由匹配、loader/action 调度、错误边界等)下沉合并进react-router核心库。到写这份 ADR 时,合并工作已基本完成,react-router@6.4.0即将发布。

此时出现了一个明显的"冗余":Remix 自身的运行时里还保留着一整套处理 Data API 的旧代码(请求分发、loader 执行、边界追踪等)。ADR 的核心目标非常直接:

把 Remix 分层(layer)到最新版 React Router 之上,从而可以删除 Remix 中大量处理 Data API 的重复代码

ADR 同时强调这不是一次"big-bang merge"(一次性大重构),而是要设计可迭代、可回滚的渐进式实施方案。

二、迁移目标拆解:四个"功能切面"及其部署约束

ADR 从"迭代发布"视角把问题拆成 4 个独立的功能方面:

  1. Server data loading(服务端数据加载)
  2. Server react component rendering(服务端 React 组件渲染)
  3. Client hydration(客户端水合)
  4. Client data loading(客户端数据加载)

四者之间存在明确的依赖关系,这是制定部署节奏的依据:

  • (1) 可以独立实现并独立部署——它只涉及服务端运行时,不依赖客户端代码同步变化;
  • (2) 和 (3) 必须一起做——因为 SSR 产出的 HTML 上下文(contexts/components)必须与客户端水合时读取的上下文严格匹配,网络两侧的 React 树结构不能"半新半旧";
  • (4) 几乎"免费"得到——一旦 (3) 中客户端创建路由时把 loaders/actions 挂了上去,客户端数据加载能力自然随之而来。

这个拆解直接决定了后文"先服务端、后渲染层"的推进顺序。

三、高层决策:四步走

ADR 的 Decision 部分给出了高层推进路线:

  1. SSR 数据加载迁移
    1. 更新handleResourceRequest,在 feature flag 之后改用createStaticHandler
      1. 目标:尽可能让单元测试与集成测试同时断言新旧两条流程
    2. 以同样方式更新handleDataRequest
    3. 以同样方式更新handleDocumentRequest,确认所有单测和集成测试通过
    4. 把新的RemixContext数据写入EntryContext,并移除旧流程
  2. @remix-run/server-runtime的改动观察稳定后再部署
  3. @remix-run/react的改动放在一个短生命周期的 feature 分支中推进
    1. 先做不带水合的服务端渲染(用RemixContext替换EntryContext
    2. 再接客户端水合
    3. 最后补上向后兼容层
  4. @remix-run/react的改动观察稳定后再部署

四、改动落点:两个包、一道"网络鸿沟"

ADR 指出需要改动的两个主要区域:

  1. @remix-run/server-runtime中服务端请求处理(主要在server.ts文件);
  2. @remix-run/react中客户端水合 + 路由(主要在components.tsserver.tsbrowser.ts文件)。

关键洞察是:这两个区域被网络(network chasm)天然隔开。服务端渲染出的 HTML 与客户端 hydration 是异步交接的,因此两侧可以各自独立实现、独立小步合并、独立开发,出问题时的回滚成本也更低——这是整份 ADR 能够"不做大爆炸式合并"的根本原因。

五、为什么先做服务端数据获取迁移

ADR 给出了两个明确理由:

  1. 改动面更小——新方案本质上只需要对接一个新 API:createStaticHandler
  2. 更容易做成 feature-flag 形式——服务端代码不受 bundle 体积约束,可以放心地在代码里保留"新旧双跑"的对照逻辑。

在此基础上,ADR 选择了绞杀者模式(strangler pattern):保留旧流程不动,在新分支逻辑中用 flag 开关双跑新流程,并通过断言证明新方案与旧方案功能等价;等建立起足够信心后,再删除旧代码和 flag 条件。

5.1 双跑对照的伪代码(ADR 原文示例)

ADR 给出的示例:flag 初始提交为false,本地开发和测试中切换为true;一旦新静态处理器(static handler)产出的 SSR 数据与旧流程不一致,就抛出异常:

// Runtime-agnostic flag to enable behavior, will always be committed as // `false` initially, and toggled to true during local dev const ENABLE_REMIX_ROUTER = false; async function handleDocumentRequest({ request }) { const appState = { trackBoundaries: true, trackCatchBoundaries: true, catchBoundaryRouteId: null, renderBoundaryRouteId: null, loaderBoundaryRouteId: null, error: undefined, catch: undefined, }; // ... do all the current stuff const serverHandoff = { actionData, appState: appState, matches: entryMatches, routeData, }; const entryContext = { ...serverHandoff, manifest: build.assets, routeModules, serverHandoffString: createServerHandoffString(serverHandoff), }; // If the flag is enabled, process the request again with the new static // handler and confirm we get the same data on the other side if (ENABLE_REMIX_ROUTER) { const staticHandler = unstable_createStaticHandler(routes); const context = await staticHandler.query(request); // Note: == only used for brevity ;) assert(entryContext.matches === context.matches); assert(entryContext.routeData === context.loaderData); assert(entryContext.actionData === context.actionData); if (catchBoundaryRouteId) { assert(appState.catch === context.errors[catchBoundaryRouteId]); } if (loaderBoundaryRouteId) { assert(appState.error === context.errors[loaderBoundaryRouteId]); } } }

注意断言覆盖的四个维度:matches(路由匹配)、routeData(loader 数据)、actionData(action 数据)、以及按边界路由 id 索引的errors(catch 边界与 error 边界各自对应)。这正是"功能等价"验收的最小完备集。

5.2 服务端的进一步迭代切分

服务端内部还可以再细分:handleResourceRequesthandleDataRequesthandleDocumentRequest三者可以独立实现(也可以独立发布),且按这个顺序推进恰好从简单到复杂

5.3 实施细节与注意事项(ADR Notes)

ADR 对 flag 方案补充了两个工程细节:

  • 不能用process.env——被改动的代码是 runtime-agnostic 的,所以先用server.ts里的本地硬编码变量,规避 runtime 特定的环境变量问题;
  • 测试需要各自的 flag 副本——例如存在"某路由 loader 只被调用一次"的单元测试,flag 开启后 loader 会被调用两次(新旧流程各一次),测试断言需要按 flag 做条件化;
  • entry.server.ts传递的remixContext形状会变化——团队将其视为不透明的(opaque)API,因此不认为这是 breaking change。

5.4 具体实现步骤(ADR Implementation approach)

  1. createHierarchicalRoutes构建 RR 的DataRouteObject实例(ADR 指向brophdawg11/rrr分支中的createStaticHandlerDataRoutes);
  2. 每个请求用unstable_createStaticHandler创建 static handler;
  3. handleResourceRequest——"应该非常简单",因为它只需把queryRoute返回的原始Response透传回去;
  4. handleDataRequest——比资源路由稍复杂,需要处理错误序列化,并把重定向(redirect)处理为客户端的 204 响应;
  5. handleDocumentRequest——最大的一个。它最终能简化很多,但不匹配点也最集中:
    • 需要把 query 的"错误"映射到 Remix 对 error/catch 的定义上,并相应向上冒泡。举例:URL/a/b/c中,若 C 导出了CatchBoundary但没有ErrorBoundary,它会被表示为hasErrorBoundary=trueDataRouteObject(因为@remix-run/router不做区分);若 C 的 loader 抛出错误,router 会在 C 的errorElement处"接住"它,但随后需要把它重新向上冒泡到最近的ErrorBoundary(ADR 指向分支中的differentiateCatchVersusErrorBoundaries);
    • 新的RemixContext:包含manifestrouteModulesstaticHandlerContextserverHandoffString;创建时与EntryContext并存并断言两者值一致;
    • 若渲染过程中捕获到错误,边界信息已被记录在staticHandlerContext上,可以用getStaticContextFromError生成第二遍渲染所需的新上下文(注意需要再次调用differentiateCatchVersusErrorBoundaries)。

六、决策在当前仓库源码中的落地验证

ADR 是 2022 年的规划,而当前仓库中这些设计已经演进为 React Router(框架模式)的标准服务端运行时,可以直接在源码中逐一印证。

1. flag 双跑消失,新流程成为唯一流程。在 packages/react-router/lib/server-runtime/server.ts 中,createRequestHandler内部通过derive()一次性完成 ADR 步骤 1 中规划的接线(L63-L71):

function derive(build: ServerBuild, mode?: string) { let dataRoutes = createStaticHandlerDataRoutes(build.routes); // ... let staticHandler = createStaticHandler(dataRoutes, { basename: build.basename, mapRouteProperties: defaultMapRouteProperties, instrumentations: build.entry.module.instrumentations, future: build.future, }); // ... }

ADR 中"先handleResourceRequest、再handleDataRequest、最后handleDocumentRequest"的顺序,如今体现为统一的请求分派逻辑(L226-L300):以.data结尾的请求走handleSingleFetchRequest,叶子路由没有default导出且没有ErrorBoundary时走handleResourceRequest,其余走handleDocumentRequest

2.createHierarchicalRoutes的设想落地为createStaticHandlerDataRoutesADR 要求把路由 manifest 转换为 RR 的DataRouteObject实例,当前实现位于 packages/react-router/lib/server-runtime/routes.ts(L48-L143)。其中值得注意的一处细节是ErrorBoundary的映射(L121-L125):

// Always include root due to default boundaries ErrorBoundary: route.id === "root" || route.module.ErrorBoundary != null ? () => null : undefined,

即根路由永远带上占位错误边界——这正是 ADR 中"router 不区分 catch/error 边界,需要在数据层标记后重新冒泡"方案的直接产物:边界信息在构建DataRouteObject时就被打上标记,供后续错误路由使用。

3.handleDocumentRequesthandleResourceRequest的形态与 ADR 描述一致。资源路由路径确实"非常简单"——server.ts 中handleResourceRequest调用staticHandler.queryRoute(request, { routeId, ... }),对结果是Response就透传、是字符串就包成Response、否则Response.json;错误处理分支中还会把 loader/action 抛出Response的情况原样返回(L697-L720),与 ADR "把 queryRoute 的原始 Response 回传"的设想吻合。文档请求路径则调用staticHandler.query(request, { requestContext, generateMiddlewareResponse, ... })(L488-L503),拿到StaticHandlerContext后组装EntryContext交给entry.server.tsx的默认导出函数渲染。

4.getStaticContextFromError的二次渲染路径被完整保留。ADR 指出:渲染中出错时,用getStaticContextFromError生成包含"错误在正确边界上"的新上下文再渲染一遍。当前实现正是这样做的(server.ts):

// Get a new StaticHandlerContext that contains the error at the right boundary context = getStaticContextFromError( staticHandler.dataRoutes, context, errorForSecondRender, );

随后重新生成entryContext(含新的staticHandlerContextserverHandoffString/serverHandoffStream),再次调用handleDocumentRequestFunction;若第二遍仍失败,则返回最终的 500 兜底响应。该函数的行为在测试 packages/react-router/tests/router/ssr-test.ts 中有专门的describe("getStaticContextFromError")用例覆盖,验证了错误被放到正确的路由边界上。

5. 新 API 的公开文档。ADR 中写作unstable_createStaticHandler的 API,如今已是稳定公开 API,见 docs/api/data-routers/createStaticHandler.md,用于为给定路由树创建query/queryRoute等能力——这正是当时"只需对接一个新 API"所指的接口。

七、第二步:UI 渲染层的整体替换与兼容策略

ADR 认为@remix-run/react的渲染层是一次"更彻底的整体替换(whole-sale replacement)",且附带向后兼容负担,所以排在第二步。但实现仍可迭代,只是不能部署迭代——SSR 与客户端 HTML 必须保持同步(相关 hooks 必须读自同一套上下文)。推进顺序:先让 SSR 文档在没有<Scripts/>的情况下正确渲染,再加入客户端水合。

主要改动包括:

  • 移除RemixEntry及其上下文,改用一个包裹DataStaticRouter/DataBrowserRouter的新RemixContext.Provider
    • 该上下文只需要 Remix 特有的部分(manifestrouteModules);
    • RemixEntryContext中的其余内容全部转移到 router 的上下文中(SSR 期间则是staticHandlerContext);
  • 完全冗余、可直接改为从react-router-dom重导出的组件FormuseFormActionuseSubmituseMatchesuseFetchers
  • 大部分冗余但需要保留 Remix 特有行为的组件(需要调整):LinkuseLoaderDatauseActionDatauseTransitionuseFetcher

向后兼容要点清单

ADR 逐条列出了必须保留的兼容行为,这也是评估"两个 router 是否真正等价"的验收清单:

  • useLoaderData/useActionData需要保留泛型(当时的react-router中它们还没有泛型);
  • useTransition需要补上submissiontype字段——因为<Form method="get">react-router-dom中不再进入 "submitting" 状态,Remix 语义下必须保留;
  • useFetcher需要补上type
  • unstable_shouldReloadshouldRevalidate取代——ADR 还留了一个开放问题:"如果两个都存在,能否优先用shouldRevalidate而兼容旧写法?"
  • error 边界与 catch 边界的区分语义必须保持;
  • Request.signal——继续以独立的signal参数传递(对应 ADR 伪代码中 loader 可拿到的取消信号)。

八、小结:一份 ADR 如何约束一次大重构

回看 decisions/0007-remix-on-react-router-6-4-0.md 的方法论,它对任何"在稳定基线上替换底层框架"的任务都有参考价值:

  1. 按部署边界拆解:利用"网络鸿沟"把问题切成服务端/客户端两个可独立回滚的战场,并把"必须一起上"的部分(SSR + hydration)明确标注;
  2. 用 strangler pattern 建立等价性证据:feature flag + 双跑 + 强断言(matches/loaderData/actionData/errors 四维对照),让"删旧代码"成为一个有测试背书的低风险动作;
  3. 从简单到复杂排序handleResourceRequesthandleDataRequesthandleDocumentRequest,复杂度递增的同时风险也递增,先易后难能尽早暴露映射不匹配的问题;
  4. 兼容清单前置:在动 UI 层之前就列出泛型、状态字段、API 更名等全部兼容点,避免渲染层替换变成黑盒。

从当前仓库源码看(packages/react-router/lib/server-runtime/server.tsroutes.ts__tests__/router/ssr-test.ts),这套方案已被完整执行:flag 双跑阶段结束后,createStaticHandler驱动的query/queryRoute流程成为服务端运行的唯一路径,而 ADR 中预留的getStaticContextFromError二次渲染机制、边界标记策略,至今仍是框架模式服务端渲染的核心骨架。

【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 16:06:22

计算机网络实验报告高分攻略:抓包、路由与Socket编程实战

简介&#xff1a;这份实验报告来自广东工业大学计算机学院&#xff0c;完整覆盖计算机网络课程中两个基础实验模块&#xff1a;Windows 下常用网络命令&#xff08;Ping、IPconfig、Netsh、Tracert、Netstat、Arp、Nslookup&#xff09;和协议分析软件基础&#xff08;Wireshar…

作者头像 李华
网站建设 2026/9/6 15:59:38

WavLM 完整使用指南:加载模型、提取特征到语音任务全流程

WavLM 完整使用指南&#xff1a;加载模型、提取特征到语音任务全流程 【免费下载链接】unilm Large-scale Self-supervised Pre-training Across Tasks, Languages, and Modalities 项目地址: https://gitcode.com/GitHub_Trending/un/unilm 如果你需要把一段 16kHz 的语…

作者头像 李华
网站建设 2026/9/6 15:58:40

重型机械行业成本管理模式分析与落地实践

简介&#xff1a;一份聚焦重型机械制造领域的成本管理专题分析文档&#xff0c;以昆明重型机械工业总公司为案例&#xff0c;面向机械制造企业管理人员、成本会计及工业经济研究者。内容聚焦成本管理现状诊断、问题成因剖析与系统成本管理模式构建&#xff0c;强调作业成本法、…

作者头像 李华
网站建设 2026/9/6 15:58:08

EBOM到MBOM转换全攻略:核心逻辑、五步操作与避坑指南

简介&#xff1a;针对企业信息化中EBOM到MBOM转换难点的专业资料&#xff0c;面向PDM/ERP实施顾问、制造企业工艺与设计人员。文档以Windchill系统为背景&#xff0c;梳理BOM定义与分类&#xff0c;剖析现有EBOM管理存在的问题&#xff0c;并通过对比超级EBOM、单一EBOM含可选件…

作者头像 李华