Epic Stack 路由系统演进:从 remix-flat-routes 迁移到 react-router-auto-routes
【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack
本篇文章以 Epic Stack 仓库中的架构决策记录 docs/decisions/045-rr-auto-routes.md 为核心,系统讲解该项目如何将路由清单生成从remix-flat-routes迁移至react-router-auto-routes,包括迁移动机、实际配置方式、文件系统路由约定(路由组、动态参数、嵌套布局、资源路由、共置模块)以及调试与验证手段。读完本文,你将掌握 Epic Stack 当前基于react-router-auto-routes的路由组织规范,能够正确新增页面、嵌套布局与资源路由,并理解这一架构决策对后续升级 React Router 的意义。
背景:为什么放弃 remix-flat-routes
Epic Stack 早期依赖remix-flat-routes将app/routes目录转换为 React Router 在构建期消费的路由清单(route manifest)。该库帮助项目引入了可预测的约定式文件路由,但在长期使用中暴露了一个结构性痛点:
它以
+后缀的混合约定将"共置(colocation)"作为默认模式,随着应用规模增长,路由目录结构容易变得不够清晰、难以组织。
与此同时,社区出现了与 React Router 原生约定和 API 对齐的react-router-auto-routes(仓库当前锁定版本为^0.8.4,见 package.json 的 devDependencies)。迁移到它的收益包括:
- 贴近上游约定:自动路由生成遵循 React Router 官方约定,降低未来升级 React Router 时的破坏风险;
- 减少自研工具面:需要维护的自定义工具链更少;
- 迁移路径更清晰:随着 React Router 自身演进,项目可以更平滑地跟进。
决策内容:替换路由清单生成器
根据 docs/decisions/045-rr-auto-routes.md 的 Decision 一节,项目正式决定:
- 用
react-router-auto-routes替换remix-flat-routes来生成应用路由清单; - 新工具纳入构建与开发流水线(
react-router build、react-router dev); - 原
remix-flat-routes的配置按新约定迁移。
该决策记录的 Status 为accepted(2025-10-15),意味着这是当前生效的既定方向,而非未定草案。
落地配置:app/routes.ts 与 react-router.config.ts
路由清单入口:app/routes.ts
迁移后的实际配置位于仓库根目录的 app/routes.ts,全文如下:
import { type RouteConfig } from '@react-router/dev/routes' import { autoRoutes } from 'react-router-auto-routes' export default autoRoutes({ ignoredRouteFiles: [ '.*', '**/*.css', '**/*.test.{js,jsx,ts,tsx}', '**/__*.*', // 这是为了让服务端工具函数与路由共置在同一目录下, // 而无需额外创建子目录。如果确实需要包含 "server" 或 // "client" 字样的路由文件名,请使用转义括号写法,例如: // my-route.[server].tsx '**/*.server.*', '**/*.client.*', ], }) satisfies RouteConfig这段配置值得逐项拆解:
| 配置项 | 作用 |
|---|---|
ignoredRouteFiles | 声明哪些文件不参与路由生成,是 glob 模式数组 |
'.*' | 忽略以点开头的隐藏文件(如.DS_Store) |
'**/*.css' | 忽略样式文件,防止.css被误当作路由 |
'**/*.test.{js,jsx,ts,tsx}' | 忽略单元测试文件,例如仓库中的 app/routes/_auth/auth.$provider/callback.test.ts |
'**/__*.*' | 忽略以双下划线开头的私有模块目录 |
'**/*.server.*' | 忽略服务端共置工具,如 app/routes/_auth/login.server.ts |
'**/*.client.*' | 忽略客户端共置工具 |
satisfies RouteConfig | 让 TypeScript 校验配置符合@react-router/dev/routes的类型约束 |
其中'**/*.server.*'/'**/*.client.*'的注释给出了一个重要约定:服务端或客户端专属工具可以与路由文件同目录共置,但要避免在文件名中使用server/client字样;万一路由本身确实需要这些关键词,用方括号转义即可,如my-route.[server].tsx。
构建期配置:react-router.config.ts
路由发现行为在 react-router.config.ts 中通过routeDiscovery: { mode: 'initial' }声明,默认开启 SSR(ssr: true)。autoRoutes生成的路由清单正是在此构建配置之下被消费的,两个文件配合共同决定了最终的 URL 结构。
文件系统路由约定:react-router-auto-routes 的命名规则
react-router-auto-routes是对 React Router 原生文件路由约定的增强实现。正如 docs/routing.md 所述,它的核心理念是"鱼与熊掌兼得":
- 路由与所使用代码共置(
+前缀的共置模块); - 保持有组织的目录结构,让相关路由按需聚合。
以下约定均有仓库真实文件佐证。
路由组(Route Groups):_前缀目录
以_开头的目录只用于组织代码,不影响 URL。典型例子:
_auth/:认证相关路由(登录、注册、找回密码等),见 app/routes/_auth;_marketing/:营销页面(首页、关于、隐私政策等),见 app/routes/_marketing;_seo/:SEO 路由(sitemap、robots.txt),见 app/routes/_seo。
例如app/routes/_auth/login.tsx对应 URL/login,_marketing/about.tsx对应/about。
动态参数:$前缀文件与目录
用$表示 URL 参数,编译期自动映射为 React Router 的:param:
$username.tsx→/users/:username;- 目录级参数:
app/routes/users/$username/notes/$noteId.tsx对应/users/:username/notes/:noteId,仓库中 app/routes/users/$username 即是真实示例。
参数可以通过 loader 的params读取(类型安全),例如 docs/skills/epic-routing/SKILL.md 中展示的:
// app/routes/users/$username/index.tsx export async function loader({ params }: Route.LoaderArgs) { const username = params.username const user = await prisma.user.findUnique({ where: { username } }) return { user } }嵌套布局:_layout.tsx
_layout.tsx为子路由提供共享布局,子路由渲染在<Outlet />位置。仓库中的嵌套布局示例:app/routes/settings/profile/_layout.tsx管理个人设置下的所有子页(change-email、connections、passkeys、password、photo、two-factor 等),app/routes/users/$username/notes/_layout.tsx则是笔记模块的二级布局。当同一段路径既需要父级 UI 又需要索引页时,就形成"布局 + index"的标准组合。
资源路由:无 UI 的路由
只导出loader或action(或两者)、不导出默认组件的文件称为资源路由,用于 API、下载、Webhook 等场景。仓库实例:app/routes/_seo/robots[.]txt.ts 与 app/routes/_seo/sitemap[.]xml.ts 用方括号转义点号,使 URL 中能保留字面量.。
共置模块:+前缀
+前缀表示"与路由共置、但不是路由本身"的模块。仓库典型示例是app/routes/users/$username/notes/+shared/目录,其中的 note-editor.tsx 是被新建与编辑笔记两条路由共用的编辑器组件(含表单校验 Schema 与MAX_UPLOAD_SIZE等逻辑)。_marketing/+logos/中的 SVG Logo 资源同理——它们放在路由目录旁却不产生任何 URL。
这与remix-flat-routes时代"+后缀"的默认共置模式恰好相反:新约定下共置是有意为之的特例,普通路由默认是层级化目录,结构更清晰。
查看生成的路由:npx react-router routes
在应用根目录运行以下命令,即可查看基于当前文件结构生成的全部路由(JSX 风格输出),这是熟悉该约定最实用的调试手段:
npx react-router routesapp/routes 的完整目录树(72 个文件、17 个目录)会生成一份对应的<Routes>树,节选关键片段:
<Routes> <Route file="root.tsx"> <Route path="*" file="routes/$.tsx" /> <Route path="auth/:provider/callback" file="routes/_auth/auth.$provider/callback.ts" /> <Route path="auth/:provider" index file="routes/_auth/auth.$provider/index.ts" /> <Route path="login" file="routes/_auth/login.tsx" /> <Route path="onboarding" index file="routes/_auth/onboarding/index.tsx" /> <Route path="robots.txt" file="routes/_seo/robots[.]txt.ts" /> <Route path="sitemap.xml" file="routes/_seo/sitemap[.]xml.ts" /> <Route path="settings/profile" file="routes/settings/profile/_layout.tsx"> <Route path="change-email" file="routes/settings/profile/change-email.tsx" /> <Route index file="routes/settings/profile/index.tsx" /> <Route path="two-factor" file="routes/settings/profile/two-factor/_layout.tsx"> <Route index file="routes/settings/profile/two-factor/index.tsx" /> <Route path="verify" file="routes/settings/profile/two-factor/verify.tsx" /> </Route> </Route> <Route path="users/:username" index file="routes/users/$username/index.tsx" /> <Route path="users/:username/notes" file="routes/users/$username/notes/_layout.tsx"> <Route path=":noteId" file="routes/users/$username/notes/$noteId.tsx" /> <Route path=":noteId/edit" file="routes/users/$username/notes/$noteId_.edit.tsx" /> <Route index file="routes/users/$username/notes/index.tsx" /> <Route path="new" file="routes/users/$username/notes/new.tsx" /> </Route> <Route path="users" index file="routes/users/index.tsx" /> </Route> </Routes>从这段输出可以印证多个约定细节:
_auth、_marketing、_seo路由组不出现在路径中;auth.$provider/callback.ts中的$provider被编译为:provider;robots[.]txt.ts的转义点号保真为robots.txt;$noteId_.edit.tsx通过$noteId_语法表示"参数段后紧跟静态段edit";_layout.tsx生成嵌套<Route>,子路由挂在其下;- 资源路由(robots、sitemap 等)与普通路由在清单中无本质区别,仅文件内容不导出组件。
编写约定速查
结合 docs/skills/epic-routing/SKILL.md 与仓库实际结构,新增路由时应遵循:
| 需求 | 文件名 / 目录 | 示例 |
|---|---|---|
| 普通页面 | about.tsx | app/routes/_marketing/about.tsx |
| 段索引页 | index.tsx | app/routes/users/index.tsx |
| 动态参数 | $param.tsx | app/routes/users/$username/index.tsx |
| 嵌套布局 | _layout.tsx | app/routes/settings/profile/_layout.tsx |
| 参数后接静态段 | $param_.action.tsx | app/routes/users/$username/notes/$noteId_.edit.tsx |
| 带点号的资源路由 | name[.]ext.ts | app/routes/_seo/robots[.]txt.ts |
| 共置工具模块 | +shared/、*.server.* | app/routes/users/$username/notes/+shared/note-editor.tsx |
同时需要注意:
- 布局组件必须渲染
<Outlet />,否则子路由不会显示; - 资源路由不要导出默认组件;
- 参数使用
$param写法,而非:param或[param]; - 路由组(
_目录)不会出现在 URL 中; - 遵循"尽量简单"原则,不要在真正需要之前就堆砌嵌套结构。
迁移的后果与验证
决策记录列出了三项核心后果:
- 升级兼容性提升:自动路由生成遵循 React Router 约定,未来升级 React Router 时破坏风险降低;
- 文档与示例同步更新:贡献者需要遵循
react-router-auto-routes的命名与组织规则,仓库因此更新了 docs/routing.md 与 docs/skills/epic-routing/SKILL.md 以反映新的目录语义; - 全量验证通过:所有既有功能经
npm run validate验证,在新路由系统下工作正常。
npm run validate(定义于 package.json)实际串联了test --run、lint、typecheck与test:e2e:run四条流水线,其中typecheck会先执行react-router typegen生成类型,再运行tsc——这保证了路由清单的类型信息与源码始终一致。而 tests/e2e 下的 Playwright 用例(如 notes.test.ts、onboarding.test.ts)则从端到端层面覆盖了这些路由的真实可访问性。
总结
从remix-flat-routes到react-router-auto-routes,Epic Stack 完成了一次"向 React Router 原生约定靠拢"的架构收敛:_前缀组织路由组、$声明动态参数、_layout.tsx构建嵌套布局、+前缀支持共置模块,配合app/routes.ts中细粒度的ignoredRouteFiles白名单与npx react-router routes的可视化输出,路由系统的可维护性与可迁移性都得到了实质提升。对使用该模板启动新项目的开发者而言,掌握这套约定,就等于掌握了整个应用页面与接口的"地图"。
【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考