ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TanStack Router 代码路由(Code-Based Routing)完全指南:用 `createRoute` 手工构建类型安全的路由树

TanStack Router 代码路由(Code-Based Routing)完全指南:用 `createRoute` 手工构建类型安全的路由树 TanStack Router 代码路由Code-Based Routing完全指南用createRoute手工构建类型安全的路由树【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router本指南围绕 TanStack Router本仓库的 React / Solid 全栈路由框架的代码路由模式展开。代码路由允许你完全绕过文件系统通过createRootRoute/createRoute/addChildren三件套以纯 TypeScript 代码手工声明路由树并保持与文件路由一致的完整类型安全与匹配语义。读完本文你将掌握代码路由的树形结构、所有路由类型根路由、基本路由、索引路由、动态段、Splat、布局、Pathless、非嵌套路由的代码写法以及它们与文件路由的对应关系和适用场景。什么是代码路由与文件路由的同源关系TanStack Router 支持两种声明路由树的方式文件路由File-Based Routing 与代码路由。两者在底层共享完全相同的路由树概念路由树负责把 URL 与正确的组件树进行匹配与组合详见 Route Trees。唯一的区别在于组织方式——文件路由用文件系统组织路由代码路由直接用代码组织。从源码层面看这一“同源”关系非常直白packages/react-router/src/fileRoute.ts中的FileRoute类在内部调用的正是createRoute工厂见 fileRoute.ts而代码路由直接导出并使用同一个工厂见 index.tsx。也就是说文件路由本质上是在代码路由之上叠加了“文件系统 代码生成”的抽象层自动生成你手工要写的那段结构。[!TIP] 官方建议绝大多数应用不推荐代码路由优先使用 文件路由。代码路由的主要价值在于理解路由树的底层原理、满足极小规模或高度动态化路由的需求。文件路由 vs 代码路由同一个树两种表达以一个相同的路由树为例文件版如下routes/ ├── __root.tsx ├── index.tsx ├── about.tsx ├── posts/ │ ├── index.tsx │ ├── $postId.tsx ├── posts.$postId.edit.tsx ├── settings/ │ ├── profile.tsx │ ├── notifications.tsx ├── _pathlessLayout.tsx ├── _pathlessLayout/ │ ├── route-a.tsx ├── ├── route-b.tsx ├── files/ │ ├── $.tsx对应的代码路由版本React 与 Solid 的 API 完全一致仅导入来源不同// React import { createRootRoute, createRoute } from tanstack/react-router // Solid // import { createRootRoute, createRoute } from tanstack/solid-router const rootRoute createRootRoute() const indexRoute createRoute({ getParentRoute: () rootRoute, path: /, }) const aboutRoute createRoute({ getParentRoute: () rootRoute, path: about, }) const postsRoute createRoute({ getParentRoute: () rootRoute, path: posts, }) const postsIndexRoute createRoute({ getParentRoute: () postsRoute, path: /, }) const postRoute createRoute({ getParentRoute: () postsRoute, path: $postId, }) const postEditorRoute createRoute({ getParentRoute: () rootRoute, path: posts/$postId/edit, }) const settingsRoute createRoute({ getParentRoute: () rootRoute, path: settings, }) const profileRoute createRoute({ getParentRoute: () settingsRoute, path: profile, }) const notificationsRoute createRoute({ getParentRoute: () settingsRoute, path: notifications, }) const pathlessLayoutRoute createRoute({ getParentRoute: () rootRoute, id: pathlessLayout, }) const pathlessLayoutARoute createRoute({ getParentRoute: () pathlessLayoutRoute, path: route-a, }) const pathlessLayoutBRoute createRoute({ getParentRoute: () pathlessLayoutRoute, path: route-b, }) const filesRoute createRoute({ getParentRoute: () rootRoute, path: files/$, })上面每个createRoute调用只声明了路由的“个体”信息父路由、路径、ID 等完整的路由树还需要通过addChildren手工组装见下文“手工构建路由树”一节。路由的解剖createRoute与getParentRoute除根路由外所有代码路由都由createRoute工厂创建const route createRoute({ getParentRoute: () rootRoute, path: /posts, component: PostsComponent, })getParentRoute是函数而非直接引用它返回当前路由的父路由。之所以设计成回调形式与 TanStack Router “魔法般”的类型安全密切相关TypeScript 需要根据父路由推导出当前路由的完整路径TFullPath、参数TParams与 IDTId等类型。从源码可见createRoute的泛型签名正是围绕TParentRoute递归推导的见 route.tsxTFullPath由ResolveFullPathTParentRoute, TPath解析、TId由ResolveIdTParentRoute, TCustomId, TPath解析、TParams由ResolveParamsTPath解析。不传入父路由TypeScript 就无从得知该路由的完整路径、可用参数与上下文类型。[!IMPORTANT] 除Root Route与Pathless Layout Route之外每条路由都必须提供path选项——这是路由与 URL pathname 进行匹配的依据。path的规范化规则代码路由在解析path时会忽略首尾斜杠索引路由的特殊路径/除外。你写不写斜杠都可以TanStack Router 会在内部统一规范化PathNormalized Path///aboutaboutabout/aboutaboutabout$$/$$/$/$手工构建路由树addChildren与文件路由不同代码路由不会自动组装路由树——文件路由的树由 bundler 插件 / CLI 在routeTree.gen.ts中自动生成而代码路由需要你显式把每个路由挂到父路由的children上/* prettier-ignore */ const routeTree rootRoute.addChildren([ indexRoute, aboutRoute, postsRoute.addChildren([ postsIndexRoute, postRoute, ]), postEditorRoute, settingsRoute.addChildren([ profileRoute, notificationsRoute, ]), pathlessLayoutRoute.addChildren([ pathlessLayoutARoute, pathlessLayoutBRoute, ]), filesRoute.addChildren([ fileRoute, ]), ]) /* prettier-ignore-end */addChildren定义于 route.ts根路由与普通路由均有此方法它返回一个新的路由实例携带完整的子路由信息最终交给createRouter({ routeTree })使用。代码路由中的各类路由概念代码路由完整支持 Routing Concepts 中介绍的全部路由类型根路由、基本路由、索引路由、动态段、Splat / Catch-All、布局路由、Pathless 路由、非嵌套路由。以下逐一给出代码写法。根路由Root Route根路由是整棵树的顶层路由封装所有其他路由作为其子节点它没有路径、始终被匹配、其component始终被渲染。代码路由中通过createRootRoute()创建// React import { createRootRoute } from tanstack/react-router const rootRoute createRootRoute()如果需要向路由注入外部上下文如 React Query 的QueryClient使用createRootRouteWithContextMyRouterContext()import { createRootRouteWithContext } from tanstack/react-router import type { QueryClient } from tanstack/react-query export interface MyRouterContext { queryClient: QueryClient } const rootRoute createRootRouteWithContextMyRouterContext()Solid 侧 API 完全一致仅将导入来源换成tanstack/solid-router与tanstack/solid-query。该工厂在 route.tsx 中实现返回的函数行为类似createRootRoute但会强制约束路由上下文类型并在创建createRouter时要求传入匹配的context见 router.ts 的类型注释。上下文机制的完整说明见 Router Context。与文件路由不同代码路由中根路由不强制导出——把整棵路由树和应用写在一个文件里虽然可行示例中常这样做以简洁演示概念但官方不推荐通常仍建议把根路由单独导出便于复用其类型与实例。基本路由Basic Routes基本路由匹配一个精确的路径段只需在createRoute中给出普通path字符串const aboutRoute createRoute({ getParentRoute: () rootRoute, path: about, })该aboutRoute将匹配 URL/about。索引路由Index Routes文件路由用index文件名表示索引路由代码路由则用单个斜杠/表示。例如路由树中的posts.index.tsx对应const postsRoute createRoute({ getParentRoute: () rootRoute, path: posts, }) const postsIndexRoute createRoute({ getParentRoute: () postsRoute, // 注意这里的单个斜杠 / path: /, })postsIndexRoute会在父路由posts被精确匹配且没有任何子路由匹配时生效匹配 URL/posts/或/posts。动态路由段Dynamic Route Segments动态段在代码路由与文件路由中完全一致给路径段加$前缀即可捕获的值会出现在路由loader或component的params对象中const postIdRoute createRoute({ getParentRoute: () postsRoute, path: $postId, // 在 loader 中使用 loader: ({ params }) fetchPost(params.postId), // 或在组件中使用 component: PostComponent, }) function PostComponent() { const { postId } postIdRoute.useParams() return divPost ID: {postId}/div }例如 URL/posts/123匹配/posts/$postId时params为{ postId: 123 }。动态段可出现在路径的每一个段上如posts/$postId/$revisionId每个$段都会捕获到params中。[!TIP] 如果组件被代码分割code-split可以用 getRouteApi 辅助函数 在其它文件中获取类型化的useParams()从而避免直接导入postIdRoute配置对象。Splat / Catch-All 路由Splat 路由的路径只有一个$它总是捕获从$位置到 URL 结尾的任意剩余 pathname捕获值存放在params._splat中const filesRoute createRoute({ getParentRoute: () rootRoute, path: files, }) const fileRoute createRoute({ getParentRoute: () filesRoute, path: $, })对于 URL/documents/hello-world配合/files/$路由params对象为{ _splat: documents/hello-world }源码中_splat在匹配阶段被写入rawParams见 new-process-route-tree.ts同时为兼容旧版还写入rawParams[*]在路径拼接阶段也同时处理_splat与*两个键见 path.ts。这正是文档提示“v1 中 splat 同时以*键兼容、v2 移除”的底层依据。布局路由Layout Routes布局路由通过把子路由嵌套在其下实现——父路由提供布局component子路由通过Outlet /渲染到布局内部const postsRoute createRoute({ getParentRoute: () rootRoute, path: posts, component: PostsLayoutComponent, // 布局组件 }) function PostsLayoutComponent() { return ( div h1Posts/h1 Outlet / /div ) } const postsIndexRoute createRoute({ getParentRoute: () postsRoute, path: /, }) const postsCreateRoute createRoute({ getParentRoute: () postsRoute, path: create, }) const routeTree rootRoute.addChildren([ // postsRoute 是布局路由 // 其子路由将被嵌套渲染在 PostsLayoutComponent 中 postsRoute.addChildren([postsIndexRoute, postsCreateRoute]), ])此时postsIndexRoute与postsCreateRoute都会渲染在PostsLayoutComponent内部// URL: /posts PostsLayoutComponent PostsIndexComponent / /PostsLayoutComponent // URL: /posts/create PostsLayoutComponent PostsCreateComponent / /PostsLayoutComponent布局路由的实际价值不止于组件包裹它还可以在展示任何子路由前强制执行loader、为子路由校验并提供 search params、提供 error 组件 / pending 元素的回退、以及向所有子路由共享 context详见 Routing Concepts 的 Layout Routes 一节。Pathless 布局路由Pathless Layout Routes文件路由用_前缀标记 pathless 布局代码路由中则直接使用id选项代替path——因为代码路由不使用文件系统组织路由自然无需_前缀来声明“无路径”const pathlessLayoutRoute createRoute({ getParentRoute: () rootRoute, id: pathlessLayout, component: PathlessLayoutComponent, }) function PathlessLayoutComponent() { return ( div h1Pathless Layout/h1 Outlet / /div ) } const pathlessLayoutARoute createRoute({ getParentRoute: () pathlessLayoutRoute, path: route-a, }) const pathlessLayoutBRoute createRoute({ getParentRoute: () pathlessLayoutRoute, path: route-b, }) const routeTree rootRoute.addChildren([ // pathless 布局路由没有 path只有 id // 因此其子路由会被嵌套在 pathless 布局路由之下 pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]), ])此时/route-a与/route-b都会渲染在PathlessLayoutComponent内部// URL: /route-a PathlessLayoutComponent RouteAComponent / /PathlessLayoutComponent // URL: /route-b PathlessLayoutComponent RouteBComponent / /PathlessLayoutComponent需要注意的是Pathless 布局路由不基于 URL 路径段匹配因此其自身路径不能包含动态段_$postId/这种写法不合法动态段必须放在 pathless 布局的父级如$postId/下的_postPathlessLayout/参见 Routing Concepts。非嵌套路由Non-Nested Routes文件路由用posts_.$postId.edit.tsx中父段后缀_表示“不嵌套”代码路由则不需要任何特殊后缀只需把完整路径写进path选项并将该路由挂到你想让它嵌套的位置此处是根路由即可// 帖子编辑器路由嵌套在根路由之下 const postEditorRoute createRoute({ getParentRoute: () rootRoute, // path 包含需要匹配的完整路径 path: posts/$postId/edit, }) const postsRoute createRoute({ getParentRoute: () rootRoute, path: posts, }) const postRoute createRoute({ getParentRoute: () postsRoute, path: $postId, }) const routeTree rootRoute.addChildren([ // 帖子编辑器路由直接挂在根路由下 postEditorRoute, postsRoute.addChildren([postRoute]), ])这样URL/posts/123/edit将渲染独立的PostEditor组件树而不是嵌套在Posts之内而/posts/123仍渲染PostsPost postId123。匹配顺序代码路由同样遵循的排序规则无论路由树如何声明Route Matching 中定义的排序规则对代码路由同样生效TanStack Router 会自动把所有路由按特异性排序先匹配最具体的路由索引路由Index Route静态路由Static Routes最具体到最不具体动态路由Dynamic Routes最长到最短Splat / 通配路由Splat/Wildcard Routes这意味着代码路由的声明顺序不影响最终匹配结果——例如即使你把$postId写在about之前/about也依然命中静态路由about而非动态路由。这一点与文件路由完全一致因为两者在底层共享同一套路由匹配引擎见 new-process-route-tree.ts 中的匹配与排序实现。小结何时使用代码路由维度文件路由代码路由组织方式文件系统 代码生成纯代码createRouteaddChildren路由树组装插件 / CLI 自动生成routeTree.gen.ts手工调用addChildren索引路由index文件名path: /Pathless 布局_前缀id选项非嵌套路由父段后缀_把完整路径写入path类型安全自动生成类型链接依赖getParentRoute递归推导适用场景大多数应用推荐极小规模、高度动态、或学习路由树原理代码路由的完整 API 可以在仓库源码中进一步研读createRoute 工厂实现、createRootRoute / createRootRouteWithContext、addChildren 定义、以及路由匹配与参数捕获引擎 new-process-route-tree.ts。若你希望保留文件路由的便捷性又需要更高的定制自由也可以参考 Virtual File Routes 这一折中方案。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表