React Router 框架化升级指南:从 RouterProvider(Data Router)迁往 Vite 插件
在 React Router 中,使用 createBrowserRouter + <RouterProvider> 手动管理路由对象属于 Data Router 用法;而由 React Router Vite 插件驱动的 Framework Mode 则额外带来 loaders/actions、类型安全路由模块、路由代码分割、滚动恢复、SSR 与预渲染等能力。本文基于 React Router 官方升级文档编写,面向已经运行着 createBrowserRouter 的应用,完整梳理从 RouterProvider 迁往 Vite 插件的七步路径:先把路由定义整理成 Route Module 形态、安装并接入插件、建立 react-router.config.ts、迁移 root.tsx 与 entry.client.tsx、把路由搬进 routes.ts,最后启动应用并视需要开启 SSR/预渲染。读完后你将掌握一套可逐步执行、每步均可独立验证的迁移路线,并且清楚每一步背后的文件约定与底层实现。
如果你的应用使用的是
<Routes>/<Route>(Component Routes,即声明式路由),请阅读 Framework Adoption from Component Routes 这份姊妹篇指南,而不是本文。
插件带来的框架特性(Features)
React Router Vite 插件(由 @react-router/dev 提供,其入口 reactRouter() 位于 packages/react-router-dev/vite.ts)把 React Router 从"纯客户端路由库"扩展为一个带数据与渲染约定的框架层,它为你的应用新增:
- Route loaders、actions 与自动数据再验证(revalidation):路由模块可导出数据加载与提交逻辑,页面交互后会触发重新验证;
- 类型安全的 Route Modules:可对 loader/action 数据、params 等自动推导类型;
- 自动路由代码分割(code-splitting):每个路由模块按需加载,显著降低首屏 bundle;
- 跨导航的自动滚动恢复:由
<ScrollRestoration />配合内部机制完成; - 可选的静态预渲染(pre-rendering);
- 可选的服务端渲染(SSR)。
文档强调:"初始搭建工作量最大,但一旦完成,你就能增量地(一次一个路由)逐步采用新特性。" 这也是整套迁移方法论的核心:转换路由定义是最耗时但收益最大的前置步骤。
前置条件(Prerequisites)
要使用该 Vite 插件,你的项目必须满足:
- Node.js 22.22.0+
- Vite 7+ 或 Vite 8+
1. 把路由定义移入路由模块(Route Modules)
React Router Vite 插件会自己渲染一个内部 RouterProvider(细节见下文第 5 步对 HydratedRouter 的分析),因此你不能再在插件环境内手写渲染一个 <RouterProvider>。取而代之的是:把你全部的路由定义按 Route Module API 格式化成路由模块文件。
这一步虽然最耗时,但即使不考虑是否采用 Vite 插件,它也有三个独立的好处:
- 路由模块会被懒加载,缩小应用初始包体积;
- 路由定义整齐统一,简化应用架构;
- 迁移是增量的——可以一次只迁移一个路由。
👉 将路由定义导出为路由模块
按 Route Module API,把路由定义的每一部分拆成独立的具名导出:
export async function clientLoader() {
return {
title: "About",
};
}
export default function About() {
let data = useLoaderData();
return <div>{data.title}</div>;
}
// clientAction, ErrorBoundary, 等等
路由模块的每个具名导出(clientLoader、clientAction、default 组件、ErrorBoundary、HydrateFallback、handle、links、meta 等)在 docs/start/framework/route-module.md 中都有详细约定:default 导出组件在路由匹配时渲染,并可通过自动生成的 Route.ComponentProps 拿到 loaderData、actionData、params、matches 四个 props;clientLoader 只在浏览器执行,可配合 serverLoader 复用服务端数据。
👉 编写一个 convert 转换函数
你的 data router(createBrowserRouter)当前期望的是 loader/action/Component 这样的字段;而 Route Module API 使用的是 clientLoader/clientAction/default 命名。因此需要写一个辅助函数做字段映射:
function convert(m: any) {
let {
clientLoader,
clientAction,
default: Component,
...rest
} = m;
return {
...rest,
loader: clientLoader,
action: clientAction,
Component,
};
}
👉 懒加载并转换你的路由模块
不要直接 import 路由模块,而是通过 lazy 懒加载并交给 convert 转换。这样路由定义既符合 Route Module API,又顺带获得了路由级代码分割的收益:
let router = createBrowserRouter([
// ... 其他路由
{
path: "about",
- loader: aboutLoader,
- Component: About,
+ lazy: () => import("./routes/about").then(convert),
},
// ... 其他路由
]);
对应用中的每个路由重复该流程。这一阶段你仍在使用旧的 createBrowserRouter 引导应用,因此每一步都可以先跑起来验证,再继续转换下一个路由。
2. 安装 Vite 插件
当所有路由定义都转成路由模块后,就可以正式引入 React Router Vite 插件。
👉 安装 React Router Vite 插件
npm install -D @react-router/dev
@react-router/dev 打包了 vite 插件、CLI(react-router dev / react-router build)以及 react-router.config.ts 的类型定义(Config 类型与各选项的默认值定义见 packages/react-router-dev/config/config.ts)。
👉 安装运行时适配器(runtime adapter)
本文假设你以 Node 作为运行时:
npm install @react-router/node
@react-router/node 提供 createReadableStreamFromReadable、文件会话存储等 Node 专属能力(参见 packages/react-router-node/index.ts),它是默认服务端入口得以在 Node 上工作所需的适配层。
👉 用 React Router 插件替换 React 插件
-import react from '@vitejs/plugin-react'
+import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
- react()
+ reactRouter()
],
});
注意:reactRouter() 插件的职责涵盖了对 React JSX 转换、HMR、模块加载等框架特性的托管,因此原来的 @vitejs/plugin-react 不再需要。
3. 新增 React Router 配置
👉 创建 react-router.config.ts
在项目根目录创建该文件。在这里你可以告诉 React Router 关于项目的信息,例如应用代码目录在哪里,以及暂时不要启用 SSR:
touch react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
appDirectory: "src",
ssr: false,
} satisfies Config;
关于这些配置项,源码给出了明确的默认值与语义(见 packages/react-router-dev/config/config.ts):
appDirectory:指向应用代码目录,相对项目根目录,默认值为"app"。你的项目源码不在app下,所以这里显式设为"src";ssr:默认值为true。设为false即启用 SPA Mode——构建期会请求/路径并输出带静态资源的index.html,使应用可以作为纯静态 SPA 部署(详见 docs/start/framework/rendering.md 与 docs/how-to/spa.md);buildDirectory:构建输出目录,默认"build";basename:应用挂载的 base path,默认"/";serverModuleFormat:服务端构建产物格式,默认"esm";prerender:可为布尔值、路径数组或返回数组的函数,也支持对象形式{ paths, concurrency }(见下文"启用 SSR 与预渲染")。
源码在 resolveConfig 中会校验 react-router.config.ts 必须以 default export 导出一个配置对象,随后把用户配置、presets 与上述默认值合并成只读(deepFreeze)的 ResolvedReactRouterConfig。同时它会强制要求应用目录下存在 root.tsx(根路由模块)与 routes.ts(路由配置文件)——这两份文件正是后续第 4、6 步要创建的东西。
4. 添加 Root 入口点(root.tsx)
在典型 Vite 应用中,index.html 是打包入口。React Router Vite 插件把入口迁移到 root.tsx 文件:这样你能用 React 来渲染应用外壳(而非静态 HTML),也为将来启用 SSR 铺路。
👉 把现有 index.html 迁移为 root.tsx
例如,如果你当前的 index.html 长这样:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
那么把上述标记搬进 src/root.tsx,并删除 index.html:
touch src/root.tsx
import {
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.0"
/>
<title>My App</title>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function Root() {
return <Outlet />;
}
root.tsx 与普通路由模块的差异在于它额外支持可选的 Layout 导出:Layout 负责渲染 <html>/<head>/<body> 外壳,而 default 导出的 Root 定义应用自身的布局组件。<Meta />、<Links />、<Scripts />、<ScrollRestoration /> 分别把各路由模块的 meta、links、脚本与滚动恢复状态聚合渲染到文档中。
👉 把 RouterProvider 之上的一切都搬进 root.tsx
全局样式、Context Provider 等应当迁入 root.tsx,以便在所有路由间共享。
例如,如果你的 App.tsx 长这样:
import "./index.css";
export default function App() {
return (
<OtherProviders>
<AppLayout>
<RouterProvider router={router} />
</AppLayout>
</OtherProviders>
);
}
你需要把 RouterProvider 之上的内容整体搬到 root.tsx:
+import "./index.css";
// ... 其他 import 与 Layout
export default function Root() {
return (
+ <OtherProviders>
+ <AppLayout>
<Outlet />
+ </AppLayout>
+ </OtherProviders>
);
}
<RouterProvider> 本身不再出现——它被插件内部渲染的 router 取代。本仓库 packages/react-router/lib/dom-export/dom-router-provider.tsx 展示了下层 RouterProvider 仅是对基础路由的轻封装;而框架模式的真正入口是下面要讲的 HydratedRouter。
5. 添加客户端入口模块(可选)
典型 Vite 应用中,index.html 把 src/main.tsx 作为客户端入口;React Router 改用一个名为 src/entry.client.tsx 的文件。
若项目不存在
entry.client.tsx,React Router Vite 插件会使用一个隐藏的默认实现。仓库中的默认入口见 packages/react-router-dev/config/defaults/entry.client.tsx,它用startTransition包裹后通过hydrateRoot(document, <StrictMode><HydratedRouter /></StrictMode>)完成水合。
👉 让 src/entry.client.tsx 成为你的入口
如果你的 src/main.tsx 长这样:
import React from "react";
import ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
} from "react-router";
import App from "./App";
const router = createBrowserRouter([
// ... route definitions
]);
ReactDOM.createRoot(
document.getElementById("root")!,
).render(
<React.StrictMode>
<RouterProvider router={router} />;
</React.StrictMode>,
);
把它重命名为 entry.client.tsx 并改成这样:
import React from "react";
import ReactDOM from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
ReactDOM.hydrateRoot(
document,
<React.StrictMode>
<HydratedRouter />
</React.StrictMode>,
);
本步有四个关键差异需要留意:
- 使用
hydrateRoot而不是createRoot; - 渲染
<HydratedRouter />而不是你的<App />组件; - 不再手动创建 routes 并把它们传给
<RouterProvider />——路由定义将在下一步迁移到routes.ts; - 来自
react-router/dom的HydratedRouter会自行读取服务端/构建期注入的全局状态(window.__reactRouterContext、window.__reactRouterManifest、window.__reactRouterRouteModules)来水合 router。仓库实现 packages/react-router/lib/dom-export/hydrated-router.tsx 中,createHydratedRouter解码水合数据流(decodeViaTurboStream)、调用createClientRoutes重建客户端路由,并通过createRouter(注意注释说明:不走createBrowserRouter,以便支持同步clientLoader流)搭建出数据路由器,最后由框架内部渲染的RouterProvider挂载。这也从源码层面印证了为什么"插件自己渲染 RouterProvider,你不能在里面再渲染一个"。
6. 迁移你的路由(routes.ts)
React Router Vite 插件使用 routes.ts 文件配置路由,其格式与你的 data router 定义相当接近。
👉 把定义迁移到 routes.ts
touch src/routes.ts src/catchall.tsx
把你的路由定义搬到 routes.ts。注意两边的 schema 并不完全一致,因此此刻会有类型错误,下一步会修正它:
+import type { RouteConfig } from "@react-router/dev/routes";
-const router = createBrowserRouter([
+export default [
{
path: "/",
lazy: () => import("./routes/layout").then(convert),
children: [
{
index: true,
lazy: () => import("./routes/home").then(convert),
},
{
path: "about",
lazy: () => import("./routes/about").then(convert),
},
{
path: "todos",
lazy: () => import("./routes/todos").then(convert),
children: [
{
path: ":id",
lazy: () =>
import("./routes/todo").then(convert),
},
],
},
],
},
-]);
+] satisfies RouteConfig;
👉 把 lazy 加载器替换为 file 加载器
在插件模式下,路由模块不再需要手写 lazy 去 import——插件会读取 file 指向的模块文件并自动完成代码分割与模块加载:
export default [
{
path: "/",
- lazy: () => import("./routes/layout").then(convert),
+ file: "./routes/layout.tsx",
children: [
{
index: true,
- lazy: () => import("./routes/home").then(convert),
+ file: "./routes/home.tsx",
},
{
path: "about",
- lazy: () => import("./routes/about").then(convert),
+ file: "./routes/about.tsx",
},
{
path: "todos",
- lazy: () => import("./routes/todos").then(convert),
+ file: "./routes/todos.tsx",
children: [
{
path: ":id",
- lazy: () => import("./routes/todo").then(convert),
+ file: "./routes/todo.tsx",
},
],
},
],
},
] satisfies RouteConfig;
此时上一阶段的 convert 函数已经功成身退:file 直接指向符合 Route Module API 的模块文件,字段映射由插件在内部完成。routes.ts 会被插件解析为路由清单:源码 resolveConfig 会把 routes.ts 的 default export 读取出来,调用 validateRouteConfig 校验合法性,再经由 configRoutesToRouteManifest 编译成以 root 为父节点的 RouteManifest——因此你的路由树必须显式(通过 file: "./routes/layout.tsx")或隐式(根路由模块 root.tsx)挂在一个根节点之下,源码也强制要求应用目录中存在 root.tsx 与 routes.ts 两个文件(对应 packages/react-router-dev/config/config.ts 的报错逻辑)。
若想进一步简化 routes.ts 定义,配置路由指南介绍了 route() 辅助函数与其它便捷写法。例如仓库在 playground 中使用 route("teams/:teamId", "./team.tsx") 这种扁平声明风格,取代冗长的对象嵌套。
7. 启动应用(Boot the app)
到这一步你应该已经完整迁移到 React Router Vite 插件。接下来更新 dev 脚本并运行应用确认一切正常。
👉 添加 dev 脚本并运行应用
"scripts": {
"dev": "react-router dev"
}
现在运行并确认应用能正常启动,再继续后续步骤:
npm run dev
同时建议把 .react-router/ 加入 .gitignore,避免把自动生成的临时文件纳入版本控制:
.react-router/
该目录是插件在开发/构建期生成路由清单与虚拟模块的位置。若需要为 params、loader data 等配置自动生成的全链路类型安全,可继续阅读 Route Module Type Safety。
启用 SSR 与/或预渲染
迁移完成后,可以用 ssr 与 prerender 两个选项控制服务端渲染与静态预渲染。ssr: true 意味着服务端构建产物需要被部署到一台真正的服务器上(通常会配合某个运行时适配器使用);prerender 则把指定路径在构建期输出为静态 HTML。
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
async prerender() {
return ["/", "/about", "/contact"];
},
} satisfies Config;
从源码看,prerender 的取值可以是布尔值、字符串数组、返回数组的异步函数,或 { paths, concurrency } 对象(concurrency 为大于 0 的整数,默认 1 即完全串行),不合法的写法会在 resolveConfig 中被拒绝。同时注意两点联动规则(同样见 resolveConfig):
ssr: false(SPA Mode)时不允许设置routeDiscovery.mode: "lazy"——无服务端时只能采用initial模式在首屏携带全部路由;prerender与serverBundles依赖 SSR 开启:源码中当ssr: false时会直接丢弃serverBundles配置。
迁移要点小结
| 迁移步骤 | 关键产物 | 目的与收益 |
|---|---|---|
| 1. 转成路由模块 | src/routes/about.tsx + convert() |
对齐 Route Module API,获得惰性加载与统一结构 |
| 2. 安装插件 | vite.config.ts 中 reactRouter() |
引入框架特性,替换 @vitejs/plugin-react |
| 3. 配置文件 | react-router.config.ts |
声明 app 目录、SSR/SPA、预渲染等构建语义 |
| 4. 根入口 | src/root.tsx |
用 React 渲染文档外壳,承载全局布局/Provider |
| 5. 客户端入口 | src/entry.client.tsx |
hydrateRoot + <HydratedRouter /> 水合 |
| 6. 路由迁移 | src/routes.ts(file 字段) |
让插件接管路由清单与代码分割 |
| 7. 启动验证 | npm run dev |
全链路联通后再继续增量采用新特性 |
最后提醒:如果本次迁移中的任一环节报错,建议按文档顺序逐步回退验证——RouterProvider 与插件各自的 RouterProvider 不能共存,root.tsx/routes.ts 是插件的硬性约定,先把应用在纯客户端(ssr: false)下跑通,再按需打开 SSR 与预渲染,是最平滑的演进路径。
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