React Router Framework Mode 全解析:Route Modules、routes.ts 配置与渲染策略实战
Framework Mode 是 React Router 的全栈模式:它基于 React Router 官方 Vite 插件,以 app/routes.ts 路由配置、路由模块(Route Modules)、自动生成的路由类型为骨架,并支持 SSR、SPA、预渲染等渲染策略。本文以仓库中的 Framework Mode 参考文档为主线,结合 playground/framework 真实示例应用与 @react-router/dev 包源码,完整覆盖其工程结构、路由配置方式、路由模块全部导出项、数据与变更(mutation)规则、表单/Fetcher 使用策略、类型安全、元数据、中间件与 RSC 扩展,帮助你从"识别一个 Framework 应用"到"正确修改它"形成完整能力。
Framework Mode 是什么:三种使用模式中的全栈选项
React Router 提供三种使用模式(Mode),特性逐层叠加——从 Declarative 到 Data 再到 Framework,获得的能力越多,架构上让渡的控制权也越多。选择哪种模式取决于"你想自己搭多少东西":
- Declarative Mode:声明式路由,
<BrowserRouter>+<Routes>,只覆盖 URL 匹配组件、导航与激活态; - Data Mode:路由配置移出 React 渲染(
createBrowserRouter),增加loader、action、useFetcher等数据能力; - Framework Mode:在 Data Mode 之上包裹 Vite 插件,额外提供类型安全的
href、类型安全的 Route Module API、智能代码分割,以及 SPA / SSR / 静态预渲染策略。
官方模式文档给出了明确的选型建议:如果你"还没有形成偏好"、在对比 Next.js / Solid Start / SvelteKit 等方案、想从 Next.js 迁移,或"就是想在 React 上快速构建",Framework Mode 是推荐起点。完整对比见 模式选择文档 与 Framework 入门索引。
识别一个 Framework Mode 应用:文件与约定
Framework 应用有一套可识别的文件形状(shape)。默认 appDirectory 为 app,但在假设具体路径之前,应先查看 react-router.config.ts 的配置。典型的 Framework 应用包含:
react-router.config.ts
app/root.tsx
app/routes.ts
app/routes/**/*.tsx
route modules importing from ./+types/...
仓库自带的 playground/framework 就是一个标准样本。它的 vite.config.ts 只做了两件事——启用 TS 路径解析并挂载官方插件:
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
resolve: { tsconfigPaths: true },
plugins: [reactRouter()],
});
react-router.config.ts 展示了配置入口的形态(含子资源完整性校验与 future 标志):
import type { Config } from "@react-router/dev/config";
export default {
subResourceIntegrity: true,
future: {
unstable_optimizeDeps: true,
},
} satisfies Config;
而 routes.ts 使用了最简手动配置:
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/_index.tsx"),
route("products/:id", "routes/product.tsx"),
] satisfies RouteConfig;
判断一个项目是否为 Framework Mode 应用,可对照这组信号:根目录有 react-router.config.ts、vite.config.ts 引入了 @react-router/dev/vite 的 reactRouter() 插件、app/ 目录下存在 routes.ts 与 root.tsx、路由模块内部从 ./+types/... 导入类型。
路由配置:routes.ts 手动配置与 flatRoutes() 文件系统路由
Framework 应用的路由统一配置在 app/routes.ts。每条路由由两部分组成:URL 模式与路由模块文件路径。配置函数由 @react-router/dev/routes 导出,从 routes.ts 源码 可以看到其完整导出为 route、index、layout、prefix、relative 及 getAppDirectory,类型包括 RouteConfig 与 RouteConfigEntry。
基础写法:route 与 index
最小可用配置(即 playground/framework 的写法):
import { type RouteConfig, route, index } from "@react-router/dev/routes";
export default [
index("./home.tsx"), // 渲染到根路由 Outlet 的 /
route("products/:pid", "./product.tsx"),
] satisfies RouteConfig;
一个更大的示例(来自 routing 文档),展示 layout 与 prefix 的组合:
import {
type RouteConfig,
route,
index,
layout,
prefix,
} from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("about", "./about.tsx"),
layout("./auth/layout.tsx", [
route("login", "./auth/login.tsx"),
route("register", "./auth/register.tsx"),
]),
...prefix("concerts", [
index("./concerts/home.tsx"),
route(":city", "./concerts/city.tsx"),
route("trending", "./concerts/trending.tsx"),
]),
] satisfies RouteConfig;
各辅助函数语义:
route(path, file, children?):常规路由,路径段自动拼到父级之下;index(file):索引路由,渲染到父路由的<Outlet>且位于父路由 URL,不能再有子路由;layout(file, children):布局路由,为子路由建立新的嵌套层级,但不向 URL 追加任何路径段——类似可以放在任意层级的"根路由";prefix("xxx", routes):给一组路由加路径前缀,本身不引入新路由节点,prefix("parent", [route("child1", ...)])等价于route("parent/child1", ...)。
嵌套路由
export default [
// parent route
route("dashboard", "./dashboard.tsx", [
// child routes
index("./home.tsx"),
route("settings", "./settings.tsx"),
]),
] satisfies RouteConfig;
父级路径会自动包含在子路由中,上述配置同时生成 /dashboard 与 /dashboard/settings;子路由通过父组件中的 <Outlet/> 渲染。
路径段:动态、可选与 Splat
- 动态段:以
:开头(如route("teams/:teamId", "./team.tsx")),匹配后解析为params.teamId供loader/action/组件 props 使用,可一条路由含多个动态段; - 可选段:段尾加
?(如route(":lang?/categories", ...)、route("users/:userId/edit?", ...)); - Splat:以
/*结尾的路径匹配其后任意字符(含/),剩余部分位于params["*"],常重命名为splat;route("*", "./catchall.tsx")可作为全局 404 兜底,在 loader 中throw new Response("Page not found", { status: 404 })。
文件系统路由:flatRoutes()
如果偏好"文件命名即路由",可用 @react-router/fs-routes 包的 flatRoutes()。其实现位于 flatRoutes.ts,从源码可以看到它支持的约定:路由模块扩展名为 .js/.jsx/.ts/.tsx/.md/.mdx,参数前缀字符 $、转义字符 []、可选段 (),并内部使用 PrefixLookupTrie(前缀查找字典树)高效匹配路由前缀。两种风格可混用:
import { type RouteConfig, route } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default [
route("/", "./home.tsx"),
...(await flatRoutes()),
] satisfies RouteConfig;
文件系统路由的完整命名约定见 file-route-conventions。
组件路由(补充说明)
也可以在任何组件树中直接使用 <Routes>/<Route> 做元素级路由,但这类路由不参与数据加载、action、代码分割等路由模块能力,使用场景比路由模块更受限。
Route Modules:Framework Mode 的核心单元
routes.ts 中引用的每个文件就是一个路由模块。它是自动代码分割、数据加载、actions、revalidation、错误边界等框架特性的载体。仓库示例 product.tsx 展示了标准形态:
import type { Route } from "./+types/product";
export function loader({ params }: Route.LoaderArgs) {
return { name: `Super cool product #${params.id}` };
}
export default function Component({ loaderData }: Route.ComponentProps) {
return <h1>{loaderData.name}</h1>;
}
一个典型的、更完整的模块示例(参考文档给出):
import type { Route } from "./+types/product";
export async function loader({ params }: Route.LoaderArgs) {
return { product: await getProduct(params.productId) };
}
export default function Product({ loaderData }: Route.ComponentProps) {
return <h1>{loaderData.product.name}</h1>;
}
全部导出项速查表
| 导出 | 用途 |
|---|---|
default |
路由匹配时渲染的路由组件 |
loader |
SSR / 预渲染 / 服务端数据请求时的服务端数据加载 |
clientLoader |
仅浏览器端的数据加载,或为服务端 loader 数据做补充 |
action |
服务端变更,由 <Form>、useSubmit 或 fetchers 触发 |
clientAction |
仅浏览器端的变更,或服务端 action 的客户端包装 |
ErrorBoundary |
本路由 loader/action/组件抛错时的 UI |
HydrateFallback |
客户端 loader 水合运行期间的初始回退 UI |
links / meta |
路由级文档 <link> 与元数据 |
handle |
任意路由元数据,通过 useMatches 消费 |
shouldRevalidate |
覆盖默认的 loader 重验证行为 |
middleware / clientMiddleware |
启用时的服务端/客户端请求管道钩子 |
各项实现细节可对照 Route Module 官方文档,其中有 middleware/clientMiddleware、headers、handle、links、meta、HydrateFallback 的完整代码示例。
ComponentProps:props 替代 hooks
default 组件渲染时自动接收 Route.ComponentProps 中定义的四组 props,可替代 useLoaderData/useParams 等 hooks,且类型自动正确:
import type { Route } from "./+types/route-name";
export default function MyRouteComponent({
loaderData,
actionData,
params,
matches,
}: Route.ComponentProps) {
// loaderData: 本模块 loader 的返回值
// actionData: 本模块 action 的返回值
// params: 路由参数
// matches: 当前路由树的完整匹配数组
return <div>...</div>;
}
服务端数据的调用位置约束
SSR / 服务端数据路由中,Node-only 或数据库代码必须放在 server-only 模块中,并从 loader/action 调用,绝不能从浏览器渲染的组件代码中调用。客户端数据加载的完整规则(含 clientLoader.hydrate = true as const 参与首屏水合的写法)见 data-loading 文档。
根路由与布局规则
app/root.tsx 是根路由,所有 routes.ts 中的路由都嵌套在它内部。全局文档/应用壳层面的关注点应放在这里:全局 Provider、应用级导航、页脚、scripts/meta/links、文档结构。分区级布局应使用嵌套/布局路由实现,不要把本应共享 UI 或数据边界的路由拍平。
playground/framework 的 root.tsx 是很好的模板参考——Layout 组件提供 <html>/<head>/<body> 结构,头部挂载 <Meta /> 与 <Links />(聚合所有路由的 meta 与 links 导出),导航链接使用 prefetch="intent",底部挂载 <ScrollRestoration /> 与 <Scripts />:
import {
Link, Links, Meta, Outlet, Scripts, ScrollRestoration,
} from "react-router";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<ul>
<li><Link prefetch="intent" to="/">Home</Link></li>
<li><Link prefetch="intent" to="/products/abc">Product</Link></li>
</ul>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
特殊文件的职责说明见 special-files 文档。
数据加载与变更(Mutations)框架规则
修改路由数据前先读 data-loading 与 actions。Framework 的核心规则:
- 用
loader或clientLoader加载路由数据; - 用
action或clientAction变更路由数据; - 路由数据优先用 loader/action,而不是临时的
useEffect抓取——这才能纳入导航生命周期、pending 状态与自动重验证; - 按文档规范使用
data()/ Responses 与 redirect; - 除非文档指向
shouldRevalidate,否则让 React Router 在 action 之后自动重验证。注意:Framework + SSR 下,loader 在每次导航与表单提交后默认自动重验证,这与 Data Mode 的行为不同(SPA Mode 下shouldRevalidate行为则与 Data Mode 一致); - 服务端路由保持 server-only 代码在独立模块中,仅从
loader/action调用。
三个常见模式
- action 校验失败:
return data({ errors, values }, { status: 400 }),然后从Route.ComponentProps["actionData"]或fetcher.data中读取并渲染错误; - loader 中记录不存在:
throw data("Not Found", { status: 404 }),由该路由的ErrorBoundary渲染; - 搜索/筛选数据:在 loader 中解析请求 URL 的 search params,让 URL 可分享、可收藏。
表单、Fetchers 与 Pending UI
涉及表单与 pending UI 时先读 actions、pending-ui、fetchers 与 form-vs-fetcher 解析。决策口诀:
| 场景 | 选择 |
|---|---|
| 更新 URL 的搜索/筛选表单 | <Form method="get"> |
| 需要改变 URL/历史或完成后重定向的变更 | <Form method="post"> |
| 用户应停留在同一页的变更 | useFetcher / <fetcher.Form> |
| 乐观 UI | 从 fetcher.formData 或 navigation.formData 派生 |
<Form>、useSubmit、fetcher 三者最终都会触发对应路由的 action 并在完成后自动重验证页面 loader 数据,差异主要在导航语义(是否推入历史、是否重定向、数据归属组件)。
类型安全:./+types 与生成类型规则
修改生成路由类型或 typed URL 行为前,先读 route-module-type-safety 与 type-safety 解析。硬性规则:
- 类型一律从
./+types/<route>导入(如import type { Route } from "./+types/product"); - 使用
Route.LoaderArgs、Route.ActionArgs、Route.ComponentProps等生成类型; - 在合适处使用 type-only import;
- 不要手工编辑生成的
.react-router/types文件——它们由 dev 工具链再生成。
playground/framework/app/routes/product.tsx 中对 params.id 的类型化访问(params: { id: string })正是生成类型带来的直接收益:动态段名称会精确推导进 loader 参数与组件 props 类型。
元数据(meta)注意事项
修改 meta 前读 meta how-to。关键点:
meta接收loaderData,不要使用已废弃的data参数;- 最后一条匹配路由的 meta 生效,且整个描述符数组是替换而非合并,可自行构建跨路由层级的 meta 组合逻辑;
- 自 React 19 起,官方文档同时推荐在路由组件中直接使用内置
<meta>/<title>元素。
渲染策略:SSR、SPA 与预渲染可混合
Framework Mode 的渲染形态可以是 SSR、SPA、预渲染,或依配置与路由行为而混合。修改渲染行为前必读:
渲染策略决定了 loader 何时在服务端执行、clientLoader 是否参与首屏水合、以及部署产物形态,是 Framework 应用中改动前必须确认的配置层面问题。
中间件、Session 与认证
实现 middleware 或 auth/session 流之前,先读 middleware 与 sessions-and-cookies。特别注意:middleware 与 context API 对版本和配置敏感——实现前先确认当前安装的 React Router 版本与应用 react-router.config.ts 中的配置。服务端 middleware 在文档与数据请求前后顺序执行,next() 继续传递调用链,到达叶子路由时执行该导航的 loader/action;客户端 clientMiddleware 是其浏览器端对应物,但不返回 Response(它不包装 HTTP 请求)。
扩展:RSC Framework
如果该 Framework 应用使用了 unstable_reactRouterRSC 或 @vitejs/plugin-rsc,则属于 RSC 变体,需额外阅读 React Server Components 指南。仓库中 playground/rsc-vite-framework 提供了完整的 RSC Framework 示例应用(含 react-router.config.ts、start.js 与 Vite 中间件启动脚本)可供参考。
上手路径:从模板到本地运行
Framework 项目最典型的起点是官方脚手架:
npx create-react-router@latest my-react-router-app
cd my-react-router-app
npm i
npm run dev
随后浏览器打开 http://localhost:5173 即可。脚手架包的实现(含交互式 prompts 与模板复制逻辑)位于 create-react-router 包,本仓库 playground/framework 则展示了模板生成的项目结构与依赖。修改任何 Framework 应用代码前,建议按"先读 start/framework 系列文档 → 确认 react-router.config.ts → 再动 routes.ts/路由模块"的顺序推进。
小结
Framework Mode 的整套工程契约可以浓缩为:vite.config.ts 挂 reactRouter() 插件 → react-router.config.ts 定义构建行为 → app/routes.ts 声明路由 → 每个路由模块通过 ./+types 获得端到端类型 → root.tsx 承载文档壳 → 渲染策略按配置混合 SSR/SPA/预渲染。掌握这套形状后,配合 how-to 与 explanation 文档(注意核对文档头部的 [MODES: framework, ...] 标记)中的具体条目,即可安全地对任何 Framework Mode 应用做数据、表单、类型与渲染层面的修改。
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 StartedRust0623
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