jk's notes
  • 路由基础

路由基础

整理时间 2025年11月19日

React Router 有三种模式:

  1. 框架模式 (功能最强大, 完整)
  2. 数据模式 (类似于 Vue 中的命令式, 由代码 + 数据来导航)
  3. 声明模式 (与 Vue 中的声明模式一致, 使用标签组件来完成导航)

这三种模式功能上是互补的. 框架模式最完成, 功能最强大. 声明式最简单, 功能也单一.

使用模板来创建的项目默认使用框架式.

路由的基础部分包含:

  • 配置路由
  • 路由模块
  • 详情
  • 组件路径

1. 配置路由

框架中, 代码目录 app 下的 routes.ts 为路由配置文件. 其内容为:

import { type RouteConfig, index } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;

注意不是 route 目录, 是 routes.ts 文件.

开发的一般模式为:

  1. 定义页面组件, 编写业务逻辑 (传统处理流程).
  2. 根据页面组件, 在 app/routers 目录下定义路由模块 (引用页面组件, 并添加辅助部分).
  3. 在 app/routes.ts 路由配置文件中, 配置路径与路由模块的映射.

文档描述路由由配置决定, 即用代码配置完成路由层级的定义. 同时文档指出可以基于文件系统来约定, 需要使用 @react-router/fs-routes 包. 用法如下:

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;

jk: 没试过, 猜测与 ASP.NET MVC 中 Controller 的处理类似, 用文件来定义路由. 待验证.

  • 框架模式定义路由表时采用辅助函数的方式来完成.
  • 数据模式定义路由表采用 JSON 对象. 操作交互与 vue-router 的模型类似.
  • 声明模式定义路由表与早期 react-router 的定义类似, 在入口处, 利用 Route 组件定义一些列路径, 然后页面加载时会从上至下匹配命中.

2. 路由模块

路由模块包含哪些内容, 文档笔记中这部分没有描述清晰, 这里给出了一个 demo, 分为三部分

  1. 引用类型.
  2. 定义 loader 组件, 为页面加载准备数据.
  3. 定义路由模块加载页面组件.

其中细节不考虑.

// provides type safety/inference
import type { Route } from "./+types/team";

// provides `loaderData` to the component
export async function loader({ params }: Route.LoaderArgs) {
  let team = await fetchTeam(params.teamId);
  return { name: team.name };
}

// renders after the loader is done
export default function Component({
  loaderData,
}: Route.ComponentProps) {
  return <h1>{loaderData.name}</h1>;
}

3. 详情

详情中包括:

  • 根路由
  • 索引路由
  • 路由与嵌套路由
  • 路由前缀与布局路由
  • 路由参数
  • 默认路由

涉及到的组件包括: index(), prefix(), layout(), route(). 路由配置类型为: RouteConfig.

导入命令为:

import {
  type RouteConfig,
  route,
  index,
  layout,
  prefix,
} from "@react-router/dev/routes";

所有的路由表辅助函数都有一个配置参数, 这些参数都是从 RouteConfigEntry 类型派生出来的:

interface RouteConfigEntry {
  /**
   * The unique id for this route.
   */
  id?: string;
  /**
   * The path this route uses to match on the URL pathname.
   */
  path?: string;
  /**
   * Should be `true` if it is an index route. This disallows child routes.
   */
  index?: boolean;
  /**
   * Should be `true` if the `path` is case-sensitive. Defaults to `false`.
   */
  caseSensitive?: boolean;
  /**
   * The path to the entry point for this route, relative to
   * `config.appDirectory`.
   */
  file: string;
  /**
   * The child routes.
   */
  children?: RouteConfigEntry[];
}

3.1 根路由

3.1.1 框架模式

在 app/root.tsx 中, 可以理解为页面的开始, 所有的内容都从这里渲染. 类似于以往 html 文件中的 div#app 的位置. 该文件中包含三个部分的内容:

  1. HTML 结构组件
  2. App 结构组件
  3. 错误处理结构组件

给人的感觉是, 这个模板下, 定义组件只管按照需求导出 (export), 不用考虑该组件怎么被加载, 怎么被导入.

默认模板中提供三个组件:

  1. Layout, 提供了 HTML 结构的模板, 页面从这里开始. 组件中使用到了 react-router 提供的 Meta, Links, ScrollRestoration, 以及 Scripts 组件.
  2. App, 提供了占位容器, 类似于 vue 中的 RouterView, 在 react 中使用 Outlet.
  3. 然后是 ErrorBoundary 组件. 好像是 react 16 引入的错误处理机制, 专门处理页面中的 JS 错误.

Layout 中的组件, 猜测目的是在页面中自动加载对应的模块(组件), 至于该组件放置的位置, 可能随意.

参考代码 (细节暂不考虑):

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>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}
export default function App() {
  return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  let message = "Oops!";
  let details = "An unexpected error occurred.";
  let stack: string | undefined;

  if (isRouteErrorResponse(error)) {
    message = error.status === 404 ? "404" : "Error";
    details =
      error.status === 404
        ? "The requested page could not be found."
        : error.statusText || details;
  } else if (import.meta.env.DEV && error && error instanceof Error) {
    details = error.message;
    stack = error.stack;
  }

  return (
    <main className="pt-16 p-4 container mx-auto">
      <h1>{message}</h1>
      <p>{details}</p>
      {stack && (
        <pre className="w-full p-4 overflow-x-auto">
          <code>{stack}</code>
        </pre>
      )}
    </main>
  );
}

3.1.2 数据模式与声明模式

根路由相当于定义一个入口, 所有的页面组件都由该入口来定义. 数据模式与声明模式没有使用该组件模板. 因此一般存在一个入口组件 main.tsx. 其中会调用 ReactDOM 的 render() 方法, 来渲染组件入口.

新版的渲染入口方法发生了变更.

createRoot(...).render(<组件></组件>)

在这里可以定义路由入口, 相当于根路由的角色. 下面是数据模式:

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RouterProvider router={routers} />
  </StrictMode>,
)

如果是声明式:

ReactDOM.createRoot(root).render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App />} />
    </Routes>
  </BrowserRouter>,
)

3.2 索引路由

3.2.1 框架模式

框架模式使用 index() 函数定义索引路由. 所谓索引路由, 就是默认路由.

  • 在顶层定义相当于匹配 /.
  • 在某个路由下定义, 例如父路由为 /parent, 相当于匹配 /parent/.

因此定义方法只用指定路由组件路径. 不需要制定路由映射用的 url 片段.

import { index, type RouteConfig } from '@react-router/dev/routes'
export default [ index('组件路径') ] satisfies RouteConfig

注意路径的入口在 app 目录下.

框架模式定义路由的一般模式为:

  1. 在 app 目录下定义页面组件. 页面组件是整个页面, 而页面内部也是由其他组件构成.
  2. 然后准备一个 app/routes 目录, 在里面定义路由组件. 路由组件引用页面组件, 必须默认导出该页面.
  3. 同时在路由组件定义文件中, 导出必要的其他模块, 如 loader, meta 等.

初步猜测, 默认组件是用于渲染页面的, 其他组件, 该模板在运行时会自动导入.

关键问题是, 路由组件中可以定义什么? 这里与渲染有很大关系.

3.2.2 数据模式

数据模式定义非常简单, 数据模式下路由表的定义步骤:

  1. 定义路由表, 路由表本质是一个复合 RouteObject 类型的数组.
  2. 在入口页面中使用 RouterProvider 部署路由, 使用 router 属性加载路由表.

索引路由类型为 IndexRouteObject, 常用属性有: index: true, Component: 路由组件.

参考代码:

import { createBrowserRouter } from "react-router"
...
createBrowserRouter([
    { index: true, Component: Home }
])

3.2.3 声明模式

声明模式配置语法模板为. 只要 <Route> 组件使用了 index 属性, 那么它就是索引路由. element 描述其渲染什么组件.

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router";
import App from "./app";

const root = document.getElementById("root");

ReactDOM.createRoot(root).render(
  <BrowserRouter>
    <Routes>
      <Route index element={<App />} />
    </Routes>
  </BrowserRouter>,
);

3.3 路由与嵌套路由

最常用的路由模式. 所谓的嵌套路由, 就是路由下的路由, 即子路由. 实现方式为:

  • 框架模式, 使用 route() 函数定义路由与子路由. 该函数第一个参数对应 url 片段, 第二个参数是组件, 第三个参数是一个数组, 用于描述子路由.
  • 数据模式, 使用对象. 对象的 path 属性是 url 片段, Component 表示组件, children 是一个数组, 用于描述子路由.
  • 声明模式则使用 <Route> 包含 <Route> 来实现. <Route> 组件的 path 属性定义 url 片段, element 属性定义渲染的组件. 由嵌套来定义子路由 (React 中, 嵌套的标签就是 props.children).

需要注意, React-Router 中严格定义了嵌套路由, 路由前缀, 与布局路由. 因此嵌套路由的父路由必须提供 <Outlet> 组件, 以渲染子路由对应的模块.

示例如下:

export default [
  // 父路由, 渲染模板 ./dashboard.tsx, 其中的 <Outlet> 用于渲染子路由对应的组件
  route("dashboard", "./dashboard.tsx", [
    // 子路由
    // 映射 dashboard
    index("./home.tsx"),
    // 映射 dashboard/settings
    route("settings", "./settings.tsx"),
  ]),
] satisfies RouteConfig;

数据模式的示例:

export default [
  {
    path: 'dashboard',
    Component: Dashboard,
    children: [
      { index: true, Component: Home },
      { path: 'settings', Component: Settings }
    ]
  }
] as RouteObject[]

声明式:

<Routes>
  <Route path="dashboard" element={<Dashboard />}>
    <Route index element={<Home />} />
    <Route path="settings" element={<Settings />} />
  </Route>
</Routes>

3.4 路由前缀与布局路由

路由前缀与布局路由相对应. 路由前缀只是在路由 url 上统一定义前置内容. 与页面组件渲染无关. 逻辑上与 vue-router 中, 不提供 component 的父路由一样, 实际上数据模式的路由前缀就是这样实现的. 而布局路由相当于给所有子路由提供了一个母版页, 但是 url 片段没有变化. 逻辑上与 vue-router 中, 将 path 设置为空字符串 '' 的路由表一样.

3.4.1 路由前缀

实现方式:

  • 框架模式的实现使用 prefix() 函数. 注意该函数返回的是数组. 配置路由表时需要解构.
  • 数据模式则是只提供 path, 不提供 Component.
  • 声明式则是让 <Route> 只提供 path, 不提供 element (实现与数据模式一致).

注意, 路由前缀仅仅是在 url 的前面添加了一节, 实现上与直接在 path 对应的地址前添加一节的逻辑是一样的.

但是路由表默认处理中会使用路由文件的字符串作为 id, 如果多个 url 映射到同一个路由组件文件上, 需要手动设置 id.

框架模式的实现:

export default [
  ...prefix("projects", [
    index("./projects/home.tsx"),
    route(":pid", "./projects/project.tsx"),
  ]),
] satisfies RouteConfig;

数据模式的实现, 只是父路由不提供 Component:

export default [
  {
    path: 'projects',
    children: [
      { index: true, Component: Home },
      { path: ':pid', Component: Project }
    ]
  }
] as RouteObject[]

声明式, 也就是父路由不使用 element:

<Routes>
  <Route path="projects">
    <Route index element={ <Home /> }></Route>
    <Route path=":pid" element={ <Project /> }></Route>
  </Route>
</Routes>

3.4.2 布局路由

布局路由相当于给子路由提供一个母版页, 但是不更新路由 url 片段. 逻辑上相当于父路由的 path="". 实现方式:

  • 框架模式下使用 layout() 函数, 该函数有两个常用参数, 一个是用作母版页组件, 另一个是子路由, 即渲染到母版页 <Outlet> 中的子模块.
  • 数据模式下, 就是让父路由不提供 path 属性.
  • 声明模式下, 就是让父路由不提供 path 属性.

需要注意的是, 父路由不提供 path, 但需要提供 Component / element. 父组件使用 <Outlet> 来容纳子组件.

框架模式下的实现方式:

export default [
  layout("./marketing/layout.tsx", [
    index("./marketing/home.tsx"),
    route("contact", "./marketing/contact.tsx"),
  ])
] satisfies RouteConfig;

数据模式的实现:

export default [
  {
    Component: Layout,
    children: [
      { index: true, Component: Home },
      { path: 'contact', Component: Contact }
    ]
  }
] as RouteObject[]

声明式实现:

<Routes>
  <Route element={ <Layout /> }>
    <Route index element={ <Home /> }></Route>
    <Route path="contact" element={ <Contact /> }></Route>
  </Route>
</Routes>

3.5 路由参数

参数有两类:

  1. 动态段, 路由路径使用 : 开头, 会被解析到 params 参数中.
  2. 可选段, 路由路径使用 ? 结尾, 表示可选.

无论是框架模式, 数据模式, 还是声明模式, 均只需要在 path 中使用 :参数名 的形式定义 url 片段即可. 要获得该参数的数据, 可以在组件参数中获取 (所有数据), 也可以使用 useParams().

export default function PageComponent(props: Route.ComponentProp) {
  const params1 = props.params
  const params2 = useParams()
  ...
}

需要验证到底使用哪种方式获得参数.

  • 在框架模式下 (使用模板生成的项目), 可以使用这两个方案获得路由参数.
  • 但是自定义的方式, 使用数据模式来获得路由参数需要使用 useParams().

一个路由中允许包含多个动态段.

可选段与动态段的适用类似. 可选段可以是静态的, 即不需要使用前缀冒号.

3.6 默认路由

默认路由是所有路由未命中的情况下匹配的路由. 也被称为 Splat 路由, catchall 段, 或 star 段.

  • 实现上就是让 path 为 /*, 它将匹配 / 后的所有任何字符串.
  • 解析的时候, 也会将其解析到 params 的 '*' 属性上.

可以将其看成一个匹配任意路由内容的字符串类型的路由参数.

解构的方式为:

const { "*": splat } = params;

3.7 动态路由

框架模式路由表由字符串来定义. 在设计使用自定义路由层级对象来生成路由表和菜单的时候相对容易. 但是数据模式下, 路由配置对象中的 Component 或 element 都需要提前导入, 那么要实现动态生成路由表就不方便了. 可选方案:

  1. Vite 环境下, 可以使用 import.meta.glob('...') 来导入所有模块, 然后用字符串映射到 Component 中 (相对复杂一些).
  2. Vite 环境下, 也可以使用动态导入 await import('...') 来导入模块.
  3. 使用 react 框架中提供的 lazy 方法来导入 (比较推荐).

3.7.1 Vite 中的 import 批量导入

import 批量导入有几种模式:

  1. 默认模式 import.meta.glob('@/views/**/*.tsx')
  2. 具名导入 import.meta.glob('@/views/**/*.tsx', { import: 'default' })

默认导入相当于获得: import('...'). 具名导入相当于: import('...').then(m => m['名字']).

const modules = import.meta.glob('./dir/*.js')

会生成类似下面的代码:

// vite 生成的代码
const modules = {
  './dir/bar.js': () => import('./dir/bar.js'),
  './dir/foo.js': () => import('./dir/foo.js')
}

具名导入:

const modules = import.meta.glob('./dir/*.js', { import: 'default' })

会转换为:

// vite 生成的代码
const modules = {
  './dir/bar.js': () => import('./dir/bar.js').then(m => m.default),
  './dir/foo.js': () => import('./dir/foo.js').then(m => m.default)
}

这个被 Vite 转换的 js 形式是核心. 将决定 react-router 如何加载.

3.7.2 动态加载

可以直接使用 import('...') 直接导入.

3.7.3 router-router 数据模式加载组件

可用方案如下:

Component: lazy(() => import('@/views/Page1'))

Component: await (() => import('@/views/Page1').then((m: any) => m.default))()

Component: await import('@/views/Page1').then((m: any) => m.default),

lazy 是从 react 导入的函数

也可以使用 react-router 的 lazy 模式:

{
  path: "...",
  lazy: async () => ({ 
    Component: await import('@/views/...').then((m: any) => m.default) 
  }),
}
Last Updated: 11/28/25, 11:10 AM
Contributors: jk