在 React Router v8 中(继承了 Remix 的架构体系),官方推荐使用 Vite 插件结合文件系统路由。通过 @react-router/fs-routes,你可以实现扁平化路由协议(Flat Routes Convention)。
下面将详细介绍如何在 React Router v8 中配置、定义和使用文件系统路由。
一、 核心配置
文件系统路由的核心原理是:通过 Vite 插件扫描 app/routes 目录下的文件,并根据特定的命名规则自动生成路由树。
1. 确保 Vite 配置正确 (vite.config.ts)
React Router v8 的文件系统路由依赖于其 Vite 插件 @react-router/dev/vite。
// vite.config.ts
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [reactRouter(), tsconfigPaths()],
});
2. 在路由入口注册文件系统路由 (app/routes.ts)
在项目的 app/routes.ts 中,通过引入 @react-router/fs-routes 的 flatRoutes 函数来启用它。
// app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes() satisfies RouteConfig;
注:flatRoutes() 会自动扫描 app/routes 文件夹并生成对应的路由表。
二、 文件命名规则与路由映射
扁平化路由规范(Flat Routes)主要通过**文件名中的点(.)和下划线(_)**来定义层级、动态参数和布局。
以下是具体的命名规则和映射关系:
1. 根路由 / 索引路由 (Index Routes)
使用 _index 表示当前路径下的默认页面。
- 文件路径:
app/routes/_index.tsx - 对应 URL:
/(首页)
2. 普通基础路由 (Basic Routes)
直接使用英文命名。
- 文件路径:
app/routes/about.tsx - 对应 URL:
/about
3. 嵌套路由与多级路径 (Nested Paths)
使用 点号 (.) 代替传统的文件夹层级。这样可以保持 routes 目录扁平化,避免深层嵌套文件夹。
- 文件路径:
app/routes/dashboard.settings.tsx - 对应 URL:
/dashboard/settings
4. 动态路由参数 (Dynamic Segments)
使用 美元符号 ($) 前缀表示动态参数。
- 文件路径:
app/routes/posts.$id.tsx - 对应 URL:
/posts/:id(例如/posts/123) - 获取参数:在组件或 Loader 中通过
params.id获取。
5. 无路径布局路由 (Pathless Layout Routes)
如果你想让一组页面共享同一个布局(Layout),但不希望在 URL 中增加路径层级,可以在布局文件名最前面加上 下划线 (_)。
- 布局文件:
app/routes/_auth.tsx(编写<Outlet />渲染子路由) - 子路由文件:
app/routes/_auth.login.tsx - 子路由文件:
app/routes/_auth.register.tsx - 对应 URL:
/login(套用_auth的布局)/register(套用_auth的布局)
6. 兜底路由 / 通配符路由 (Splat / Catch-all)
使用单美元符号 $ 捕获所有未匹配的路径。
- 文件路径:
app/routes/$.tsx - 对应 URL:
/*(通常用于 404 页面)
三、 路由文件内的编写规范
在定义好文件结构后,每个路由文件(如 app/routes/posts.$id.tsx)的结构通常包含:数据加载器(Loader)、动作处理器(Action)和页面组件(Default Export)。
// app/routes/posts.$id.tsx
import { useLoaderData, useParams } from "react-router";
import type { Route } from "./+types/posts.$id"; // 自动生成的类型定义
// 1. Loader:在服务端(或客户端)渲染前获取数据
export async function loader({ params }: Route.LoaderArgs) {
const postId = params.id;
const post = await fetch(`https://api.example.com/posts/${postId}`).then(res => res.json());
return { post };
}
// 2. 页面组件
export default function PostDetail() {
const { post } = useLoaderData<typeof loader>();
const params = useParams();
return (
<article>
<h1>{post.title}</h1>
<p>当前文章 ID: {params.id}</p>
<div>{post.content}</div>
</article>
);
}
四、 基于文件夹的协同定位 (Co-location)
如果你觉得把所有路由文件都平铺在 app/routes 下会导致单个目录下文件过多,或者你想把样式表、子组件、测试文件和路由组件放在一起,React Router 支持“文件夹路由”。
只要将文件夹命名为对应的路由名称,并在内部创建 route.tsx 即可:
- 传统文件写法:
app/routes/about.tsx - 等价文件夹写法:
app/ └── routes/ └── about/ ├── route.tsx (必须命名为 route.tsx 作为入口) ├── about-header.tsx (局部子组件) └── styles.css (局部样式)
这两种写法生成的路由完全一致,方便你根据项目规模自由选择。
总结对照表
文件路径 (在 app/routes/ 下) |
对应 URL 路径 | 是否包含父布局 |
|---|---|---|
_index.tsx |
/ |
根布局 |
about.tsx |
/about |
根布局 |
dashboard.tsx |
/dashboard (充当布局) |
根布局 |
dashboard.stats.tsx |
/dashboard/stats |
嵌套在 dashboard.tsx 内 |
_auth.tsx |
(无,仅作布局) | 根布局 |
_auth.login.tsx |
/login |
嵌套在 _auth.tsx 内 |
users.$id.tsx |
/users/:id |
根布局 |
$.tsx |
/any-wrong-path (404) |
根布局 |