首页
/ React Router 框架化升级指南:从 RouterProvider(Data Router)迁往 Vite 插件

React Router 框架化升级指南:从 RouterProvider(Data Router)迁往 Vite 插件

2026-09-07 20:22:49作者:卓炯娓

在 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.tsxentry.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, 等等

路由模块的每个具名导出(clientLoaderclientActiondefault 组件、ErrorBoundaryHydrateFallbackhandlelinksmeta 等)在 docs/start/framework/route-module.md 中都有详细约定:default 导出组件在路由匹配时渲染,并可通过自动生成的 Route.ComponentProps 拿到 loaderDataactionDataparamsmatches 四个 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.mddocs/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.htmlsrc/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/domHydratedRouter 会自行读取服务端/构建期注入的全局状态(window.__reactRouterContextwindow.__reactRouterManifestwindow.__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.tsxroutes.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 与/或预渲染

迁移完成后,可以用 ssrprerender 两个选项控制服务端渲染与静态预渲染。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 模式在首屏携带全部路由;
  • prerenderserverBundles 依赖 SSR 开启:源码中当 ssr: false 时会直接丢弃 serverBundles 配置。

迁移要点小结

迁移步骤 关键产物 目的与收益
1. 转成路由模块 src/routes/about.tsx + convert() 对齐 Route Module API,获得惰性加载与统一结构
2. 安装插件 vite.config.tsreactRouter() 引入框架特性,替换 @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.tsfile 字段) 让插件接管路由清单与代码分割
7. 启动验证 npm run dev 全链路联通后再继续增量采用新特性

最后提醒:如果本次迁移中的任一环节报错,建议按文档顺序逐步回退验证——RouterProvider 与插件各自的 RouterProvider 不能共存,root.tsx/routes.ts 是插件的硬性约定,先把应用在纯客户端(ssr: false)下跑通,再按需打开 SSR 与预渲染,是最平滑的演进路径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388