ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

@react-router/fs-routes 演进与实现剖析:从版本史读懂 React Router 文件系统路由约定

@react-router/fs-routes 演进与实现剖析:从版本史读懂 React Router 文件系统路由约定 react-router/fs-routes 演进与实现剖析从版本史读懂 React Router 文件系统路由约定【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-routerreact-router/fs-routes是 React Router 官方提供的文件系统路由File System Routing工具负责按 Remix v2 的约定从目录结构自动生成路由配置供routes.ts使用。本文以该包的 CHANGELOG 为主线逐版本梳理其演进脉络并深入到 index.ts、flatRoutes.ts、manifest.ts 等源码讲解其 API 设计、命名约定解析与冲突检测机制让你既能掌握版本升级的影响面也能理解文件系统路由背后的工作原理。一、包定位路由配置与文件系统之间的「编译器」在 React Router 7 的框架模式中路由不再以组件树形式内联书写而是集中到一个routes.ts模块中导出RouteConfigEntry[]形式的配置数组。react-router/fs-routes正是为这种配置模式服务的你只要把路由文件按约定放进app/routes目录它就能把「目录 文件名」翻译成等价的路由配置省去手工维护routes.ts的成本。安装方式与官方 README 一致npm install react-router/fs-routes当前仓库中该包的 package.json 显示其运行时依赖只有一个minimatch^10.2.5用于把ignoredRouteFiles通配规则编译成正则react-router/dev是 peer 依赖Node 版本要求22.22.0TypeScript peer 范围为^5.1.0 || ^6.0.0 || ^7.0.0可选依赖。这一点与 CHANGELOG 中 v8.0.0 提升 Node 下限、v8.3.0 放开typescript7的变更一一对应后文会细说。二、完整版本演进时间线继承 CHANGELOG 全量记录该包的 CHANGELOG.md 从 v7.0.0 的初始发布记录到 v8.3.0。多数 Patch 版本属于与react-router/dev同步发布的依赖更新每次发布都会把react-router/dev提升到相同版本而少数版本携带了独立的行为修复。完整梳理如下。7.0.0 —— 初始发布作为随 React Router v7 一起发布的独立包首次亮相。初始功能即包含flatRoutes它沿用了 Remix v2 的 routes 文件命名约定可读取app/routes下的文件并生成路由配置。同时将react-router/dev升级到 7.0.0。7.1.0 —— 路由目录缺失时显式报错若routes目录不存在flatRoutes将抛出明确错误而不是静默返回空路由。对应源码逻辑位于 flatRoutes.ts 的flatRoutes函数中if (!fs.existsSync(routesDir)) { throw new Error( Could not find the routes directory: ${routesDir}. Did you forget to create it?, ); }注意一个前置细节flatRoutes会先查找app目录下的root路由模块支持.js/.jsx/.ts/.tsx/.md/.mdx找不到也会抛错。这意味着从 7.1.0 起目录缺漏问题会在构建/开发启动阶段被快速暴露。7.6.3 —— 用replaceAll规范化 Windows 路径该版本修复了 Windows 文件系统下路径分隔符处理。此前路径规范化依赖逐字符替换现在改用String.prototype.replaceAll。对应实现见独立的 normalizeSlashes.tsimport path from node:path; export function normalizeSlashes(file: string) { return file.replaceAll(path.win32.sep, /); }由于文件名解析过程对.、/、\都视作段分隔符见后文isSegmentSeparator在 Windows 上若不先把\统一为/路由 ID 与路径拼接就会产生歧义。replaceAll一次性完成全部替换也消除了旧的循环替换写法可能遗漏连续分隔符的隐患。7.13.0 —— 修复 routes 目录位于 app 目录之外的场景此前若把rootDirectory配置到 app 目录外部路由文件的相对路径计算可能出错。该版本修复了这一问题。从 index.ts 的实现可以看到它如何容忍目录外移let { ignoredRouteFiles [], rootDirectory: userRootDirectory routes } options; let appDirectory getAppDirectory(); let rootDirectory path.resolve(appDirectory, userRootDirectory); let relativeRootDirectory path.relative(appDirectory, rootDirectory); let prefix normalizeSlashes(relativeRootDirectory);userRootDirectory先经path.resolve相对 app 目录解析成绝对路径再算回相对 app 的路径作为prefix因此即使目录落在 app 外部此时relativeRootDirectory形如../shared-routes后续的匹配与 ID 计算仍能基于一致的相对路径展开。7.14.1 —— 在 peer 依赖范围中加入 TypeScript 6将 peerDependencies 的 TypeScript 范围扩展为同时支持 TS 5 与 TS 6保证使用新版 TypeScript 的项目不会触发 peer 依赖告警。8.0.0 —— 主版本升级Node 22.22 与依赖翻新两个重要变化将最低 Node 版本提升到22.22.0反映在 package.json 的engines字段将minimatch从^9.0.0升级到^10.2.5以匹配新版react-router/dev的要求。minimatch承担着把忽略规则转成正则的重任见 flatRoutes.tslet ignoredFileRegex Array.from(new Set([**/.*, ...ignoredFilePatterns])) .map((re) makeRe(re)) .filter((re: any): re is RegExp !!re);它内部恒定注入**/.*忽略所有以点开头的隐藏文件再合并用户传入的忽略模式用makeRe编译为多个RegExp随后逐文件regex.test(relativePath)判定是否排除。依赖主版本升级意味着 glob 语法细节如字符集、负向模式行为可能与 9.x 存在差异升级后如需校验忽略规则可参考 flatRoutes-test.ts 中针对 ignored 文件的断言用例。7.14.x–7.18.x / 8.1.x–8.3.x —— 与react-router/dev保持同步7.14.0 起 CHANGELOG 标题从7.14.0调整为7.14.1、7.15.0等8.0.0 之后各版本8.0.1、8.1.0、8.2.0、8.3.0延续「Patch 同步依赖」节奏。其中 8.3.0 额外放开对typescript7的支持peer 范围更新为^5.1.0 || ^6.0.0 || ^7.0.0。版本演进速查表版本类型核心变更7.0.0Major随 React Router v7 初始发布7.1.0Patchroutes目录缺失时flatRoutes抛错7.6.3Patch用replaceAll规范化 Windows 路径分隔符7.13.0Patch修复路由目录位于 app 目录外的路径问题7.14.1Patchpeer 依赖加入 TypeScript 6 支持8.0.0Major最低 Node 22.22.0minimatch升至^10.2.58.3.0Patch放开typescript7使用其余 7.x、8.x 版本均为「Patch 同步升级react-router/dev」未携带独立行为变更。三、API 与接入方式在 routes.ts 中挂载文件路由react-router/fs-routes的公开入口只有一个异步函数flatRoutes见 index.ts签名如下export async function flatRoutes( options: { /** minimatch glob 数组匹配到的文件将被忽略默认 [] */ ignoredRouteFiles?: string[]; /** 文件系统路由目录相对 app 目录默认 ./routes */ rootDirectory?: string; } {}, ): PromiseRouteConfigEntry[];在框架式应用中典型的接入方式是把它放进routes.ts的路由数组中用法详见 file-route-conventions.md 的 Setting up 一节import { type RouteConfig } from react-router/dev/routes; import { flatRoutes } from react-router/fs-routes; export const routes: RouteConfig [ ...(await flatRoutes()), ];与内置routes/约定一样默认读取app/routes目录。如需换目录配置rootDirectory...(await flatRoutes({ rootDirectory: file-routes, }))ignoredRouteFiles则用于排除某些不希望成为路由的文件例如保留测试桩或占位组件...(await flatRoutes({ ignoredRouteFiles: [home.tsx], }))值得注意的一个默认行为路由目录不存在时入口函数并不会抛错而是安全降级为返回空配置let routes fs.existsSync(rootDirectory) ? flatRoutesImpl(appDirectory, ignoredRouteFiles, prefix) : {}; return routeManifestToRouteConfig(routes);也就是说「目录缺失直接抛错」只在flatRoutes解析内部被触发的路径上生效若目录本来就不存在外层会宽容地视作「暂无路由」。这与 7.1.0 引入的报错语义并不冲突前者针对「你声明了文件路由但目录没建好」的误配置后者针对「目录真的没被创建」的冷启动场景。四、源码级原理文件名如何变成路由配置整体调用链分三层恰好对应三个核心源文件flatRoutes(index.ts) —— 解析 options、定位目录、产出 RouteConfigEntry[] └─ flatRoutesImpl(flatRoutes.ts) —— 扫描目录、解析命名、建路由树 RouteManifest └─ routeManifestToRouteConfig(manifest.ts) —— RouteManifest → RouteConfigEntry[]第一层扫描与忽略flatRoutes.ts 的flatRoutesfs.readdirSync只读取 routes 目录的一层条目不递归遍历目录类型条目会被当作「文件夹路由」处理——在文件夹内寻找route或index模块文件并检测二者同时存在时的冲突。每一条目先经过忽略正则过滤再进入命名解析。routeModuleExts支持.js/.jsx/.ts/.tsx/.md/.mdx意味着 Markdown/MDX 文件同样可作为路由模块。第二层命名约定解析getRouteSegments与createRoutePath核心是把 routeId相对 app 目录的路径如routes/posts.$slug切成段并翻译为 URL path。解析器是一个有限状态机状态在NORMAL / ESCAPE / OPTIONAL / OPTIONAL_ESCAPE之间迁移对应源码中的type State处理四种特殊语法$param→:param动态段段首单独的$在文件末尾时映射为*通配否则映射为:[literal]→ 转义内容按字面字符处理如[.]、[sitemap.xml]、[](segment)→ 可选段翻译为末尾带?的 path 段_layout→ pathless 布局段在生成路径时被跳过createRoutePath中segment.startsWith(_)即continue结尾_如app_→ 退出父级布局嵌套仅作为路径段存在_index→ 标记该路由为 index 路由createRoutePath会去掉最后一段命名映射的完整行为可在 flatRoutes-test.ts 中得到逐一验证。例如该测试数据表中的映射关系路由文件名生成的 pathroutes.$slugroutes/:slugroutes.$routes/*_indexundefinedindex 路由$slug[.]json:slug.jsonsub.[sitemap.xml]sub/sitemap.xmlposts.$slug.[image.jpg]posts/:slug/image.jpg(routes).($slug)routes?/:slug?user_.projects.$id.roadmapuser/projects/:id/roadmap若段内出现不被支持的*、:或/例如routes/about.[*].tsx解析器会抛出形如Route segment ... for ... cannot contain *的错误——测试中专门针对非法斜杠与非法通配文件做了toThrow断言。这正是文件名路由的价值非法 URL 结构在开发期即被拦截而不是运行时才 404。第三层父子关系与冲突检测flatRoutesUniversal构建路由树使用了一个字符级PrefixLookupTrie所有 routeId 按长度降序排序后依次入树并用findAndRemove找到「以当前 routeId 为前缀」的后代路由从而把parentId指向父级最终所有无父路由统一挂到root之下。同层 URL 冲突会被检测并告警如routes/parent._pathless.foo.tsx与routes/parent._pathless2.foo.tsx都对应parent/foo报错文案由 flatRoutes.ts 中的getRoutePathConflictErrorMessage生成形如⚠️ Route Path Collision: /parent/foo The following routes all define the same URL, only the first one will be used ... ⭕️ ...但设计上特意放行了「pathless 布局路由」文件名最后一段以_开头且不是_index之间的同 path——源码注释解释了原因account._private.tsx与account._public.tsx会合法地共享/account分别承载私有/公开两套互斥子路由。从源码结构看这一豁免是为了支持同层级多套无路径布局的常见需求同时仍能捕获非布局路由的真实冲突。收尾RouteManifest → RouteConfigEntry[]最后 manifest.ts 中routeManifestToRouteConfig把扁平 map 转成树状数组parentId root的条目成为顶层路由其余条目作为children挂到父配置上返回标准的RouteConfigEntry[]可直接并入routes.ts。__tests__/routeManifestToRouteConfig-test.ts对该转换的正确性有专门覆盖。五、与其他配置方式的取舍react-router/fs-routes并非唯一的文件路由实现——react-router 仓库中还提供了react-router/remix-routes-option-adapter在routes.ts中直接使用 Remix 风格的 routes 选项 API 定义的兼容层其 defineRoutes.ts 负责把嵌套回调转成配置以及框架内置的默认routes目录能力。区别在于fs-routes面向「约定了目录即路由」的开发者代码零样板手动配置则保留完全的程序化控制力。对于需要混合两种思路的项目完全可以在routes.ts中把flatRoutes()的产物与其他手工RouteConfigEntry拼进同一个数组。六、升级与维护建议从 CHANGELOG 可以看出该包的两条维护主线跟随react-router/dev版本同步发布——几乎每个版本都会同步依赖升级时应保持三者react-router、react-router/dev、react-router/fs-routes主版本一致独立 bug 修复集中在路径/文件系统边界——Windows 分隔符、目录外移、目录缺失这三类问题表明该包的心智模型高度依赖路径规范化升级后若出现路由数量与预期不符应优先检查app/routes目录位置、文件名中的隐藏点文件以及ignoredRouteFiles规则是否与新 minimatch 版本语法兼容。如需验证安装版本后行为是否符合预期可参考 flatRoutes-test.ts命名映射、忽略规则、冲突报错的断言与 routeManifestToRouteConfig-test.ts配置树组装把它们当作规范来校准自己的目录结构完整命名约定文档见 docs/how-to/file-route-conventions.md。【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表