news 2026/9/15 1:13:32

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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手工组装(见下文“手工构建路由树”一节)。

路由的解剖:createRoutegetParentRoute

除根路由外,所有代码路由都由createRoute工厂创建:

const route = createRoute({ getParentRoute: () => rootRoute, path: '/posts', component: PostsComponent, })

getParentRoute是函数而非直接引用,它返回当前路由的父路由。之所以设计成回调形式,与 TanStack Router “魔法般”的类型安全密切相关:TypeScript 需要根据父路由推导出当前路由的完整路径(TFullPath)、参数(TParams)与 ID(TId)等类型。从源码可见,createRoute的泛型签名正是围绕TParentRoute递归推导的(见 route.tsx):TFullPathResolveFullPath<TParentRoute, TPath>解析、TIdResolveId<TParentRoute, TCustomId, TPath>解析、TParamsResolveParams<TPath>解析。不传入父路由,TypeScript 就无从得知该路由的完整路径、可用参数与上下文类型。

[!IMPORTANT] 除Root RoutePathless Layout Route之外,每条路由都必须提供path选项——这是路由与 URL pathname 进行匹配的依据。

path的规范化规则

代码路由在解析path时会忽略首尾斜杠(索引路由的特殊路径/除外)。你写不写斜杠都可以,TanStack Router 会在内部统一规范化:

PathNormalized Path
//
/aboutabout
about/about
aboutabout
$$
/$$
/$/$

手工构建路由树: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),使用createRootRouteWithContext<MyRouterContext>()

import { createRootRouteWithContext } from '@tanstack/react-router' import type { QueryClient } from '@tanstack/react-query' export interface MyRouterContext { queryClient: QueryClient } const rootRoute = createRootRouteWithContext<MyRouterContext>()

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)

动态段在代码路由与文件路由中完全一致:给路径段加$前缀即可,捕获的值会出现在路由loadercomponentparams对象中:

const postIdRoute = createRoute({ getParentRoute: () => postsRoute, path: '$postId', // 在 loader 中使用 loader: ({ params }) => fetchPost(params.postId), // 或在组件中使用 component: PostComponent, }) function PostComponent() { const { postId } = postIdRoute.useParams() return <div>Post 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> <h1>Posts</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]), ])

此时postsIndexRoutepostsCreateRoute都会渲染在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> <h1>Pathless 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仍渲染<Posts><Post postId="123">

匹配顺序:代码路由同样遵循的排序规则

无论路由树如何声明,Route Matching 中定义的排序规则对代码路由同样生效:TanStack Router 会自动把所有路由按特异性排序,先匹配最具体的路由

  1. 索引路由(Index Route)
  2. 静态路由(Static Routes,最具体到最不具体)
  3. 动态路由(Dynamic Routes,最长到最短)
  4. Splat / 通配路由(Splat/Wildcard Routes)

这意味着代码路由的声明顺序不影响最终匹配结果——例如即使你把$postId写在about之前,/about也依然命中静态路由about而非动态路由。这一点与文件路由完全一致,因为两者在底层共享同一套路由匹配引擎(见 new-process-route-tree.ts 中的匹配与排序实现)。

小结:何时使用代码路由

维度文件路由代码路由
组织方式文件系统 + 代码生成纯代码(createRoute+addChildren
路由树组装插件 / 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 1:12:27

GPT2微调实战:从零构建春节对联自动生成系统

简介&#xff1a;基于GPT2的春节对联自动生成系统&#xff0c;聚焦中文对联创作&#xff0c;面向NLP开发者、深度学习者及传统文化传播者。系统借助transformers库与深度学习技术&#xff0c;在自定义春节期间对联数据集上反复训练&#xff0c;使模型掌握对仗工整、平仄相谐的生…

作者头像 李华
网站建设 2026/9/15 1:11:21

GD32F303独立开发指南:时钟/外设/Flash全栈避坑实践

简介&#xff1a;本资源是面向嵌入式初学者与GD32F303单片机开发者的完整软硬件入门套件&#xff0c;覆盖芯片选型、外设驱动开发与工程实践全链路。压缩包含652个文件&#xff0c;总计24.52MB&#xff0c;其中C源码&#xff08;251个&#xff09;与头文件&#xff08;284个&am…

作者头像 李华
网站建设 2026/9/15 1:11:14

Kvasir-SEG+YOLOv8单类别息肉检测实战指南

简介&#xff1a;本资源是面向医学图像AI初学者与计算机视觉实践者的YOLO格式息肉检测专用数据集&#xff0c;基于Kvasir-SEG公开数据构建&#xff0c;专为单类别&#xff08;息肉&#xff09;目标检测任务优化&#xff0c;可直接用于模型训练、验证与可视化调试。压缩包共2000…

作者头像 李华
网站建设 2026/9/15 1:09:59

公积金缴纳比例对实际收入的影响分析

1. 公积金缴纳比例差异解析 最近帮朋友分析offer时发现一个有趣现象&#xff1a;两家公司提供的月薪都是2万&#xff0c;但公积金缴纳比例一家是12%&#xff0c;另一家只有5%。粗看似乎差别不大&#xff0c;但实际计算后才发现&#xff0c;这个差异对实际收入的影响远超想象。 …

作者头像 李华
网站建设 2026/9/15 1:06:07

专科生必备:8款实测有效的降AI检测率工具推荐

1. 项目概述作为一名专科院校的学生&#xff0c;在学术写作和日常作业中&#xff0c;降低AI检测率&#xff08;即让内容看起来更像人工创作&#xff09;已经成为一项必备技能。随着AI写作工具的普及&#xff0c;教育机构对AI生成内容的检测也越来越严格。本文将分享8款经过实测…

作者头像 李华
网站建设 2026/9/15 1:05:59

微信小程序骰子游戏:从零掌握生命周期与状态管理

简介&#xff1a;本资源是一个面向微信小程序初学者的轻量级实战项目——“投骰子”小游戏&#xff0c;适用于移动开发入门者、前端学习者及微信生态开发者&#xff0c;帮助快速掌握小程序核心开发范式。压缩包共12个文件&#xff08;9KB&#xff09;&#xff0c;包含4个JS逻辑…

作者头像 李华