尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Epic Stack 路由体系实战:基于 react-router-auto-routes 的文件路由与代码就近组织

Epic Stack 路由体系实战:基于 react-router-auto-routes 的文件路由与代码就近组织 Epic Stack 路由体系实战基于 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 的路由方案展开它没有使用 React Router 的内置路由约定而是基于react-router-auto-routes实现了文件系统路由在「路由与其依赖代码就近共存」与「按需组织的目录结构」之间取得平衡。读完本文你将掌握 Epic Stack 的路由目录组织规则、npx react-router routes的调试技巧、路由分组 / 参数 / 嵌套布局 / 资源路由等核心用法并能从源码层面理解路由清单是如何生成的。一、概述文件路由 自动路由清单Epic Stack 使用 React Router 作为路由内核当前仓库中react-router版本为^7.16.0见 package.json但没有使用 React Router 自带的路由约定而是选择了react-router-auto-routes仓库中版本为^0.8.4。这是一个对 React Router 约定做了特殊增强的实现在标准文件路由的基础上增加了若干能力。react-router-auto-routes的核心收益是「鱼与熊掌兼得」路由与其使用的代码就近共存Colocation每个路由模块可以把自己的组件、样式、服务端工具放在一起不需要跨目录引用保持有组织的目录结构路由仍可以按业务模块分组存放结构清晰、便于维护。其配置位于仓库根目录的 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}, **/__*.*, // This is for server-side utilities you want to colocate // next to your routes without making an additional // directory. If you need a route that includes server or // client in the filename, use the escape brackets like: // my-route.[server].tsx **/*.server.*, **/*.client.*, ], }) satisfies RouteConfigautoRoutes()接收一个配置对象并返回符合RouteConfig的路由清单。其中ignoredRouteFiles决定了哪些文件不会被当作路由文件是理解 Epic Stack 路由目录的关键忽略模式作用.*忽略点开头的隐藏文件**/*.css忽略样式文件**/*.test.{js,jsx,ts,tsx}忽略测试文件如app/routes/users/$username/index.test.tsx**/__*.*忽略__前缀的目录 / 文件**/*.server.*忽略与服务端工具就近共存的.server.文件**/*.client.*忽略与客户端工具就近共存的.client.文件这里有一个值得注意的设计由于**/*.server.*和**/*.client.*会被整体忽略如果你确实需要一个包含 server 或 client 字样的真实路由文件名就需要借助转义括号例如my-route.[server].tsx这正是_seo分组中robots[.]txt.ts、sitemap[.]xml.ts这类写法的同款机制。该方案取代了项目此前使用的remix-flat-routes。根据 docs/decisions/045-rr-auto-routes.md 的决策记录react-router-auto-routes与 React Router 的原生约定和 API 对齐降低了自定义工具链的维护面也为未来随 React Router 升级提供了更清晰的迁移路径。二、快速上手用npx react-router routes查看真实路由清单作为路由约定的使用者最重要的调试工具是在仓库根目录运行npx react-router routes它会以近似 JSX 的形式输出根据当前文件结构生成的完整路由清单。也就是说你无需记忆任何约定直接就能看到文件系统最终会被编译成哪些路由、路径参数是什么、嵌套关系如何。2.1 Epic Stack 当前的路由目录结构以下是编写本文时 Epic Stack 的app/routes目录全貌对应仓库中的实际文件见 app/routesapp/routes ├── $.tsx ├── me.tsx ├── _auth │ ├── forgot-password.tsx │ ├── login.tsx │ ├── logout.tsx │ ├── reset-password.tsx │ ├── signup.tsx │ ├── verify.tsx │ ├── auth.$provider │ │ ├── callback.ts │ │ └── index.ts │ ├── onboarding │ │ ├── $provider.tsx │ │ └── index.tsx │ └── webauthn │ ├── authentication.ts │ └── registration.ts ├── _marketing │ ├── about.tsx │ ├── index.tsx │ ├── privacy.tsx │ ├── support.tsx │ ├── tos.tsx │ └── logos │ ├── logos.ts │ └── ... ├── _seo │ ├── robots[.]txt.ts │ └── sitemap[.]xml.ts ├── admin │ └── cache │ ├── index.tsx │ ├── lru.$cacheKey.ts │ ├── sqlite.$cacheKey.ts │ └── sqlite.tsx ├── resources │ ├── download-user-data.tsx │ ├── healthcheck.tsx │ ├── images.tsx │ └── theme-switch.tsx ├── settings │ └── profile │ ├── _layout.tsx │ ├── change-email.tsx │ ├── connections.tsx │ ├── index.tsx │ ├── passkeys.tsx │ ├── password.tsx │ ├── password_.create.tsx │ ├── photo.tsx │ └── two-factor │ ├── _layout.tsx │ ├── disable.tsx │ ├── index.tsx │ └── verify.tsx └── users ├── index.tsx └── $username ├── index.tsx └── notes ├── $noteId.tsx ├── $noteId_.edit.tsx ├── _layout.tsx ├── index.tsx └── new.tsx 17 directories, 72 files注意app/routes下还有未被npx react-router routes视为路由的辅助文件例如onboarding/$provider.server.ts、webauthn/utils.server.ts、notes/shared/note-editor.tsx等它们正是被ignoredRouteFiles排除、或使用前缀标记的就近共存模块不会产生路由。2.2 对应的完整路由清单JSX 输出npx react-router routes针对上述目录输出的路由树如下Routes Route fileroot.tsx Route path* fileroutes/$.tsx / Route pathauth/:provider/callback fileroutes/_auth/auth.$provider/callback.ts / Route pathauth/:provider index fileroutes/_auth/auth.$provider/index.ts / Route pathforgot-password fileroutes/_auth/forgot-password.tsx / Route pathlogin fileroutes/_auth/login.tsx / Route pathlogout fileroutes/_auth/logout.tsx / Route pathonboarding/:provider fileroutes/_auth/onboarding/$provider.tsx / Route pathonboarding index fileroutes/_auth/onboarding/index.tsx / Route pathreset-password fileroutes/_auth/reset-password.tsx / Route pathsignup fileroutes/_auth/signup.tsx / Route pathverify fileroutes/_auth/verify.tsx / Route pathwebauthn/authentication fileroutes/_auth/webauthn/authentication.ts / Route pathwebauthn/registration fileroutes/_auth/webauthn/registration.ts / Route pathabout fileroutes/_marketing/about.tsx / Route index fileroutes/_marketing/index.tsx / Route pathprivacy fileroutes/_marketing/privacy.tsx / Route pathsupport fileroutes/_marketing/support.tsx / Route pathtos fileroutes/_marketing/tos.tsx / Route pathrobots.txt fileroutes/_seo/robots[.]txt.ts / Route pathsitemap.xml fileroutes/_seo/sitemap[.]xml.ts / Route pathadmin/cache index fileroutes/admin/cache/index.tsx / Route pathadmin/cache/lru/:cacheKey fileroutes/admin/cache/lru.$cacheKey.ts / Route pathadmin/cache/sqlite fileroutes/admin/cache/sqlite.tsx Route path:cacheKey fileroutes/admin/cache/sqlite.$cacheKey.ts / /Route Route pathme fileroutes/me.tsx / Route pathresources/download-user-data fileroutes/resources/download-user-data.tsx / Route pathresources/healthcheck fileroutes/resources/healthcheck.tsx / Route pathresources/images fileroutes/resources/images.tsx / Route pathresources/theme-switch fileroutes/resources/theme-switch.tsx / Route pathsettings/profile fileroutes/settings/profile/_layout.tsx Route pathchange-email fileroutes/settings/profile/change-email.tsx / Route pathconnections fileroutes/settings/profile/connections.tsx / Route index fileroutes/settings/profile/index.tsx / Route pathpasskeys fileroutes/settings/profile/passkeys.tsx / Route pathpassword fileroutes/settings/profile/password.tsx / Route pathpassword/create fileroutes/settings/profile/password_.create.tsx / Route pathphoto fileroutes/settings/profile/photo.tsx / Route pathtwo-factor fileroutes/settings/profile/two-factor/_layout.tsx Route pathdisable fileroutes/settings/profile/two-factor/disable.tsx / Route index fileroutes/settings/profile/two-factor/index.tsx / Route pathverify fileroutes/settings/profile/two-factor/verify.tsx / /Route /Route Route pathusers/:username index fileroutes/users/$username/index.tsx / Route pathusers/:username/notes fileroutes/users/$username/notes/_layout.tsx Route path:noteId fileroutes/users/$username/notes/$noteId.tsx / Route path:noteId/edit fileroutes/users/$username/notes/$noteId_.edit.tsx / Route index fileroutes/users/$username/notes/index.tsx / Route pathnew fileroutes/users/$username/notes/new.tsx / /Route Route pathusers index fileroutes/users/index.tsx / /Route /Routes这个输出是「文件结构 → 实际 URL」之间最权威的映射新增路由后随时可以运行该命令核对结果。三、核心约定详解从文件到 URL 的映射规则3.1 路由分组Route Groups_前缀目录不进 URL以_开头的目录是路由分组仅用于组织代码不会出现在 URL 中。从上面的清单可以清楚看到_auth/forgot-password.tsx→/forgot-password_marketing/index.tsx→/分组内的index.tsx仍作为根路由_marketing/about.tsx→/about_seo/robots[.]txt.ts→/robots.txt_seo/sitemap[.]xml.ts→/sitemap.xmlEpic Stack 用分组把「认证、营销页、SEO」三类职责截然不同的路由分开管理同时不污染 URL 空间。3.2 动态参数$前缀文件名映射为:param$开头的文件名或目录名会成为 URL 参数段文件URLusers/$username/index.tsx/users/:usernameauth.$provider/index.ts/auth/:provideradmin/cache/lru.$cacheKey.ts/admin/cache/lru/:cacheKeyusers/$username/notes/$noteId.tsx/users/:username/notes/:noteId在 loader 中通过params读取参数类型安全// app/routes/users/$username/index.tsx export async function loader({ params }: Route.LoaderArgs) { const username params.username // Type-safe! const user await prisma.user.findUnique({ where: { username }, }) return { user } }3.3 布局路由_layout.tsx_layout.tsx为同级及所有子级路由提供共享布局子路由渲染在Outlet /位置。仓库中最典型的例子是 app/routes/users/$username/notes/_layout.tsx它在 loader 中按params.username查询笔记主人左侧渲染导航含「New Note」与笔记列表右侧通过Outlet /承载index.tsx、$noteId.tsx、$noteId_.edit.tsx、new.tsx四个子路由并提供了带 404 处理的ErrorBoundary。在清单输出中可见其嵌套形态Route pathusers/:username/notes fileroutes/users/$username/notes/_layout.tsx Route path:noteId fileroutes/users/$username/notes/$noteId.tsx / Route path:noteId/edit fileroutes/users/$username/notes/$noteId_.edit.tsx / Route index fileroutes/users/$username/notes/index.tsx / Route pathnew fileroutes/users/$username/notes/new.tsx / /Route3.4 段后缀Pathless 后缀$param_.xxx与password_.create当「带有参数的父级路由」与「同一层级的固定路径路由」都需要index语义时约定用_结尾来终止参数段继承$noteId_.edit.tsx→/users/:username/notes/:noteId/edit注意$noteId后面的_表示该段不继续传递参数子段是平级路径而非更深一层的参数password_.create.tsx→/settings/profile/password/create与password.tsx/settings/profile/password同级共存避免password/create被误解析为password参数下的子路由。参考 app/routes/settings/profile/password_.create.tsx 与 app/routes/users/$username/notes/$noteId_.edit.tsx。3.5 转义括号[.]与.server.文件名方括号用于让特殊字符「字面化」使文件系统无法表达的 URL 片段得以生成robots[.]txt.ts→/robots.txtsitemap[.]xml.ts→/sitemap.xml对应源码 app/routes/_seo/robots[.]txt.ts 通过 loader 返回 robots 内容app/routes/_seo/sitemap[.]xml.ts 生成站点地图。同理若未来需要名为my-route.server.tsx的真实路由应写成my-route.[server].tsx否则会被ignoredRouteFiles中的**/*.server.*忽略。3.6 通配路由$.tsx根目录下的 app/routes/$.tsx 对应path*用于兜底匹配所有未命中路由通常配合错误边界使用。四、就近共存Colocation与前缀如果你熟悉 React Router 原生约定可以把react-router-auto-routes简单理解为它把前缀用来标记「就近共存的非路由模块」。这是与原生约定最关键的区别——原生约定中文件默认都是路由而这里只有不带前缀、且不被ignoredRouteFiles排除的文件才会成为路由。前缀目录中的文件不会被编译为路由只作为普通模块被路由引用。仓库中的典型实例app/routes/users/$username/notes/ ├── _layout.tsx # 布局路由带 loader ├── index.tsx # 笔记列表 ├── $noteId.tsx # 笔记详情 ├── $noteId_.edit.tsx # 笔记编辑 ├── new.tsx # 新建笔记 └── shared/ # 路由间共享的代码非路由 ├── note-editor.tsx # 共享编辑器组件 └── note-editor.server.tsx_marketing/logos/logos.ts同样是前缀共存的模块营销页 logo 数据不会生成/logos路由。五、路由中的 loader / action / 资源路由实战5.1 loader 与 actionloaderGET 请求时在渲染前加载数据返回结果注入loaderDataaction处理 POST / PUT / DELETE 等数据变更请求。典型 loader 组件模式export async function loader({ request, params }: Route.LoaderArgs) { const userId await requireUserId(request) const data await prisma.something.findMany({ where: { userId } }) return { data } } export default function RouteComponent({ loaderData }: Route.ComponentProps) { return div{/* 使用 loaderData.data */}/div }典型 action 模式表单提交后redirectexport async function action({ request }: Route.ActionArgs) { const userId await requireUserId(request) const formData await request.formData() await prisma.something.create({ data: { /* ... */ } }) return redirect(/success) }5.2 资源路由Resource Routes只导出 loader / action资源路由不导出default组件只导出loader或action用于 API、下载、webhook、健康检查等场景。app/routes/resources分组下的四个路由全部是资源路由。最典型的 app/routes/resources/healthcheck.tsx// learn more: https://fly.io/docs/reference/configuration/#services-http_checks import { prisma } from #app/utils/db.server.ts import { type Route } from ./types/healthcheck.ts export async function loader({ request }: Route.LoaderArgs) { const host request.headers.get(X-Forwarded-Host) ?? request.headers.get(host) try { // if we can connect to the database and make a simple query // and make a HEAD request to ourselves, then were good. // Prefer SELECT 1 over User.count(): count() was producing Slow DB Query // Sentry insights on the tiny Fly demo VM without indicating app issues. await Promise.all([ prisma.$queryRawSELECT 1, fetch(${new URL(request.url).protocol}${host}, { method: HEAD, headers: { X-Healthcheck: true }, }).then((r) { if (!r.ok) return Promise.reject(r) }), ]) return new Response(OK) } catch (error: unknown) { console.log(healthcheck ❌, { error }) return new Response(ERROR, { status: 500 }) } }它同时验证数据库连通性prisma.$queryRawSELECT 1与自身可访问性HEAD 请求成功返回OK失败返回 500 的ERROR。resources/images.tsx、resources/download-user-data.tsx、resources/theme-switch.tsx分别承担图片服务、用户数据下载与主题切换接口全部不渲染 UI。5.3 Search Params查询参数通过useSearchParams读写适合搜索、分页等场景import { useSearchParams } from react-router export default function SearchPage() { const [searchParams, setSearchParams] useSearchParams() const query searchParams.get(q) || const page Number(searchParams.get(page) || 1) return ( div input value{query} onChange{(e) setSearchParams({ q: e.target.value })} / {/* Results */} /div ) }六、命名约定速查表文件名模式含义示例_folder/路由分组不进 URL_auth/login.tsx→/login_layout.tsx子路由共享布局settings/profile/_layout.tsxindex.tsx当前路径段的根路由users/index.tsx→/users$param.tsxURL 参数段$username→:username$param_.xxx带参数的段后缀_终止参数传递$noteId_.edit.tsx[.]/[server]转义括号字面化特殊字符robots[.]txt.ts→/robots.txtfolder/就近共存的非路由模块notes/shared/note-editor.tsx$.tsx通配路由path*兜底 404 / 错误处理七、设计原则与常见误区7.1 路由设计哲学能简则简结合 docs/skills/epic-routing/SKILL.md 中记录的项目路由哲学尽可能少做保持路由结构简单除非确有需要不要创建复杂的嵌套路由先简单出现明确收益再加复杂度避免过度设计不要「以防万一」地提前创建抽象或复杂结构。例如简单用户页用users/$username.tsx单文件即可而像笔记这种确实需要列表 / 详情 / 编辑 / 新建多个页面的场景才值得使用users/$username/notes/嵌套目录结构。7.2 常见误区清单❌ 使用 React Router 原生约定而不是react-router-auto-routesEpic Stack 不是原生约定❌ 在资源路由中导出default组件资源路由只应导出loader/action❌ 忘记在布局中放置Outlet /导致子路由无法渲染❌ 参数文件写成:param.tsx或[param].tsx正确写法是$param.tsx❌ 把路由分组_auth/的目录名拼进 URL分组不进 URL❌ 使用参数前不校验其存在性❌ 为简单场景提前创建嵌套布局 / 共享组件等抽象。八、与构建管线的衔接路由清单在构建与开发阶段由 app/routes.ts 的autoRoutes()生成并由 react-router.config.ts 中的 React Router 配置消费export default { // Defaults to true. Set to false to enable SPA for all routes. ssr: true, routeDiscovery: { mode: initial }, future: { unstable_optimizeDeps: true, }, // ... } satisfies Config其中routeDiscovery: { mode: initial }意味着路由清单在构建初期一次性生成基于autoRoutes输出的文件映射而非运行时动态发现ssr: true表明所有路由默认开启服务端渲染。开发时新增 / 移动路由文件npx react-router routes的输出会随之更新可直接用于核对映射关系。根布局与错误边界定义于 app/root.tsx路由的 SEO 补充可参考 app/routes.ts 与 app/routes/_seo 的实现。九、延伸阅读React Router 路由决策记录从remix-flat-routes迁移到react-router-auto-routes的背景与取舍路由技能文档更细的路由模式、常见示例与反模式清单app/routes.tsautoRoutes配置与ignoredRouteFilesapp/routes/users/$username/notes/_layout.tsx嵌套布局 参数 错误边界示例app/routes/resources/healthcheck.tsx资源路由无 UI示例app/routes/_seo/robots[.]txt.ts 与 app/routes/_seo/sitemap[.]xml.ts转义括号文件名示例。提示react-router-auto-routes更完整的约定细节以其官方文档为准本项目文档仅覆盖 Epic Stack 实际使用到的子集在你自己的路由结构发生变化后请始终以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),仅供参考
返回列表