首页 / TanStack 生态入门教程 / 代码式路由

TanStack 生态入门教程

代码式路由

本教程共 38 篇 · 第 17 篇 · 更新于 2026-07-27 · 约 12 分钟阅读

TanStackTanStack 生态入门教程TanStack Router代码式路由createRoute虚拟文件路由路由树createRouter

17. 代码式路由

本节目标:学会用 createRoute 手动构建路由树,搞懂 getParentRoutepathid 三要素的关系,了解虚拟文件路由把文件式和代码式混用。学完你能在不能用文件式路由的场景下手动搭路由,也能在文件式里局部用代码式。

17.1 什么时候用代码式路由

上一章讲了文件式路由,官方推荐,省心省力。那为啥还要学代码式?

Note

官方原话:代码式路由不推荐用于大多数应用,建议用文件式路由。但有些场景代码式更合适。

代码式路由适合这些场景:

  • 老项目接入:已有自己的目录组织方式,不想按 TanStack 的命名约定重组文件。
  • 路由完全程序化:路由结构要从配置文件、后端接口动态生成,不是写死的。
  • 特殊组织需求:要把路由定义和组件放一起,或者按业务模块拆分得非常细。
  • 学习原理:理解代码式能更深入理解文件式背后在干啥(文件式本质是生成代码式代码)。

文件式路由其实是代码式路由的超集—它用文件命名约定加代码生成,自动产出代码式路由代码。理解了代码式,文件式的很多行为就通了。

17.2 代码式路由的核心三要素

代码式路由用 createRoute 创建每个路由,三个关键选项要搞清:

  • getParentRoute:函数,返回这个路由的父路由。必填,类型安全靠它。
  • path:路由路径(除了根路由和无路径布局路由)。匹配 URL 用。
  • id:路由的唯一标识。无路径布局路由用 id 代替 path

先看最简单的根路由和基础路由:

import { createRootRoute, createRoute } from '@tanstack/react-router'

// 根路由:不传 path
const rootRoute = createRootRoute()

// 基础路由:传 path,getParentRoute 指向 rootRoute
const aboutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'about',
})
Warning

每个非根、非无路径布局的路由都必须有 pathgetParentRoute 是函数(返回路由),不是路由本身。写成 getParentRoute: rootRoute 会报错,得写成 getParentRoute: () => rootRoute

17.2.1 path 的规范化

path 写不写斜杠都行,TanStack Router 内部会规范化:

你写的 path规范化后
//(索引路由特殊保留)
/aboutabout
about/about
aboutabout
$$(通配)
/$$

所以 path: 'about'path: '/about'path: 'about/' 效果一样。但索引路由的 path: '/' 不能省,那个斜杠是索引的标志。

17.3 各种路由用代码怎么写

第 15 章讲过路由类型,这里看每种类型用代码式怎么实现。和文件式对照着看更清楚。

17.3.1 根路由

和文件式一样,调 createRootRoute()

import { createRootRoute, Outlet } from '@tanstack/react-router'

const rootRoute = createRootRoute({
  component: () => <div><Outlet /></div>,
})

要上下文用 createRootRouteWithContext<T>()

import { createRootRouteWithContext } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'

interface MyRouterContext {
  queryClient: QueryClient
}

const rootRoute = createRootRouteWithContext<MyRouterContext>()({
  component: () => <div><Outlet /></div>,
})

第 21 章讲 Router 和 Query 集成时会用到这个带上下文的根路由。

17.3.2 索引路由

文件式用 index 文件名标记,代码式用 path: '/'

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts',
})

const postsIndexRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '/', // 索引路由,匹配 /posts 精确访问
})

postsIndexRoute 匹配 /posts/posts/

17.3.3 动态路由

$ 前缀的段是动态参数,和文件式一样:

const postRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '$postId',
  loader: ({ params }) => fetchPost(params.postId),
  component: PostComponent,
})

function PostComponent() {
  const { postId } = postRoute.useParams()
  return <div>文章 ID:{postId}</div>
}

$ 能出现在任意段,/posts/$postId/$revisionId 也行。

Tip

如果组件被代码分割(懒加载),不能直接 import postRoute。这时用 getRouteApi 帮手在别的文件里拿到类型安全的 useParams,第 18 章会讲。

17.3.4 通配路由

path: '$' 是通配,匹配从这到结尾的所有内容:

const filesRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'files',
})

const fileRoute = createRoute({
  getParentRoute: () => filesRoute,
  path: '$',
})

访问 /files/documents/hello-worldparams._splat'documents/hello-world'

17.3.5 布局路由

代码式里,“有子路由的路由”就是布局路由。给父路由配 component,组件里放 <Outlet />,子路由挂上去就自动套布局:

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts',
  component: PostsLayoutComponent,
})

function PostsLayoutComponent() {
  return (
    <div>
      <h1>文章区</h1>
      <Outlet />
    </div>
  )
}

const postsIndexRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '/',
})

const postsCreateRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: 'create',
})

访问 /posts 渲染 <PostsLayoutComponent><PostsIndex /></PostsLayoutComponent>,访问 /posts/create 渲染 <PostsLayoutComponent><PostsCreate /></PostsLayoutComponent>

17.3.6 无路径布局路由

文件式用 _ 前缀,代码式用 id 代替 path

const pathlessLayoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  id: 'pathlessLayout', // 用 id 而不是 path
  component: PathlessLayoutComponent,
})

function PathlessLayoutComponent() {
  return (
    <div>
      <h1>无路径布局</h1>
      <Outlet />
    </div>
  )
}

const pathlessARoute = createRoute({
  getParentRoute: () => pathlessLayoutRoute,
  path: 'route-a',
})

const pathlessBRoute = createRoute({
  getParentRoute: () => pathlessLayoutRoute,
  path: 'route-b',
})

id 是路由的唯一标识,因为无路径路由不参与 URL 匹配,得有个 ID 让 TypeScript 区分。访问 /route-a 渲染 <PathlessLayout><RouteA /></PathlessLayout>,URL 里没 pathlessLayout

17.3.7 非嵌套路由

文件式用 _ 后缀脱离父级,代码式不用特殊语法,靠怎么挂路由树完整路径实现:

// 文章编辑路由,直接挂根路由下,path 写完整
const postEditorRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts/$postId/edit', // 完整路径
})

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts',
})

const postRoute = createRoute({
  getParentRoute: () => postsRoute,
  path: '$postId',
})

const routeTree = rootRoute.addChildren([
  postEditorRoute, // 挂根下,不套 <Posts>
  postsRoute.addChildren([postRoute]), // 套 <Posts>
])

postEditorRoutepath'posts/$postId/edit'(完整路径),getParentRouterootRoute,所以它匹配 /posts/123/edit 但不套 <Posts> 组件。这种”URL 上属于某组但布局独立”的需求,代码式很自然就能表达。

17.4 手动构建路由树

代码式路由不像文件式那样自动生成路由树,得自己用 addChildren 拼:

const routeTree = rootRoute.addChildren([
  indexRoute,
  aboutRoute,
  postsRoute.addChildren([
    postsIndexRoute,
    postRoute,
  ]),
  postEditorRoute,
  settingsRoute.addChildren([
    profileRoute,
    notificationsRoute,
  ]),
  pathlessLayoutRoute.addChildren([
    pathlessARoute,
    pathlessBRoute,
  ]),
  filesRoute.addChildren([fileRoute]),
])

addChildren 接收一个数组,数组元素可以是路由,也可以是”路由.addChildren([…])”这种带子路由的路由。层层嵌套下去就是路由树。

Warning

addChildren 不会自动把路由的父子关系对齐。你定义 postRoute 时写了 getParentRoute: () => postsRoute,但路由树里忘了把 postRoute 放进 postsRoute.addChildren([...]),类型会出错。两边的父子关系要一致。

17.5 创建 Router 并注册类型

路由树拼好后,创建 Router 实例:

import { createRouter } from '@tanstack/react-router'

const router = createRouter({
  routeTree,
  defaultPreload: 'intent',
})

// 注册类型,让 TypeScript 认识你的路由
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

declare module 这段和文件式一样,不能漏。注册后 <Link to="/posts"> 才能检查路由是否存在。

挂载到应用:

import { RouterProvider } from '@tanstack/react-router'
import ReactDOM from 'react-dom/client'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <RouterProvider router={router} />,
)

17.6 虚拟文件路由:两种方式混用

有时候你既想用文件式的省心,又想在某些地方用代码式的灵活。TanStack Router 提供了虚拟文件路由(Virtual File Routes),让你用代码描述路由树,但路由组件还是引用真实文件。

适用场景:

  • 已有路由组织方式,想保留。
  • 想自定义路由文件位置。
  • 想完全覆盖文件式路由生成,搞自己的约定。

17.6.1 安装和配置

虚拟文件路由用 @tanstack/virtual-file-routes 包。配 Vite 插件时传 virtualRouteConfig

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      virtualRouteConfig: './routes.ts', // 指向虚拟路由配置文件
    }),
    react(),
  ],
})

17.6.2 用工具函数描述路由树

routes.ts 里用 rootRouterouteindexlayoutphysical 这些函数描述路由树,每个函数引用一个真实文件:

import {
  rootRoute,
  route,
  index,
  layout,
  physical,
} from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  index('index.tsx'),
  layout('pathlessLayout.tsx', [
    route('/dashboard', 'app/dashboard.tsx', [
      index('app/dashboard-index.tsx'),
      route('/invoices', 'app/dashboard-invoices.tsx', [
        index('app/invoices-index.tsx'),
        route('$id', 'app/invoice-detail.tsx'),
      ]),
    ]),
    physical('/posts', 'posts'), // 挂载一个文件式目录
  ]),
])

各函数作用:

  • rootRoute(file, children):虚拟根路由,指向根组件文件。
  • route(path, file, children?):普通路由,path 是 URL 路径,file 是组件文件。能省略 file 只设路径前缀。
  • index(file):索引路由。
  • layout(file, children, id?):无路径布局路由,可选自定义 id
  • physical(path, dir):把一个文件式路由目录挂到指定路径下。

17.6.3 physical:在虚拟路由里嵌入文件式

physical 是虚拟文件路由最香的地方。你的大部分路由用文件式(省心),少数特殊地方用虚拟路由自定义,两者能共存:

export const routes = rootRoute('root.tsx', [
  index('index.tsx'),
  layout('pathlessLayout.tsx', [
    route('/dashboard', 'app/dashboard.tsx', [
      index('app/dashboard-index.tsx'),
    ]),
    // posts 目录用文件式,挂到 /posts 下
    physical('/posts', 'posts'),
  ]),
])

posts 目录下按文件式命名约定放文件(index.tsx$postId.tsx 等),自动生成 /posts/posts/$postId 这些路由。外面用虚拟路由自定义结构,里面用文件式省心。

Tip

physical 还能不带路径前缀,把一个目录的路由合并到当前层级:physical('features')features/ 目录的路由并到当前层,不加 URL 前缀。适合按业务模块分目录但 URL 平级的情况。

17.6.4 在文件式里嵌入虚拟路由

反过来也行:主体用文件式,某个子目录用虚拟路由。在那个目录里放个 __virtual.ts 文件,里面用 defineVirtualSubtreeConfig 描述子树:

routes/
├── __root.tsx
├── foo/
│   ├── bar/
│   │   ├── __virtual.ts     # 这个目录用虚拟路由
│   │   ├── details.tsx
│   │   ├── home.tsx
│   │   └── route.ts
│   └── bar.tsx
└── index.tsx
import { defineVirtualSubtreeConfig, index, route } from '@tanstack/virtual-file-routes'

export default defineVirtualSubtreeConfig([
  index('home.tsx'),
  route('$id', 'details.tsx'),
])

defineVirtualSubtreeConfig 里不用 rootRoute(因为不是根),只描述这个子树的结构。子树里还能再嵌套文件式目录,甚至再嵌套虚拟路由,套娃无限。

17.7 代码式 vs 文件式:怎么选

看张对比表:

维度文件式代码式
配置方式文件命名约定手写 createRoute
路由树生成插件自动手动 addChildren
代码量
类型安全强(自动生成类型链路)强(但手写易漏)
灵活性受命名约定约束完全自由
适合场景新项目、标准结构老项目、动态路由、特殊组织
自动代码分割默认支持需手动配置
学习成本命名规则要记概念直接但要写更多代码

选择建议

  • 新项目、没有特殊需求:用文件式,省心省力,官方推荐。
  • 老项目接入、目录结构不想改:用代码式或虚拟文件路由。
  • 路由结构动态生成(比如从后端配置读):用代码式
  • 主体用文件式但局部要自定义:用虚拟文件路由__virtual.ts 子树。
  • 主体用自定义结构但部分用文件式:用虚拟文件路由physical 挂载。
Note

不管选哪种,核心概念(路由树、路由匹配、loader、搜索参数)都是一样的。区别只在”路由树怎么搭”。后面章节讲的内容两种方式都适用。

17.8 常见坑

坑一:getParentRoute 写成路由本身。 要写函数 () => rootRoute,不能写 rootRoute。写成路由本身类型推导会出错。

坑二:addChildrengetParentRoute 不一致。 getParentRoute 写的父路由,和 addChildren 里挂的父路由必须一致。不一致类型会报错。

坑三:无路径布局路由忘了用 id 无路径布局路由不能用 path,得用 idid 是唯一标识,名字不能和其他路由重复。

坑四:非嵌套路由路径写不完整。 postEditorRoute 要脱离 posts 嵌套,path 必须写完整的 'posts/$postId/edit',不能只写 'edit'。只写 'edit' 就变成 posts 的子路由了。

坑五:代码式忘了代码分割。 文件式默认自动代码分割,代码式不会。要代码分割得用 React.lazy 配合路由的 component 选项手动配置。

坑六:虚拟文件路由的 route 路径里下划线当字面量。 文件式里 _ 前缀有特殊含义(无路径布局),但虚拟路由的 route('/about', 'about.tsx') 里路径是字面量,_ 就是普通字符。要无路径布局用 layout 函数,别在 route 路径里用 _

17.9 小结

代码式路由的内容理一遍:

  • createRoute 创建路由,三要素:getParentRoutepathid
  • 各种路由类型的代码写法:根、基础、索引、动态、通配、布局、无路径布局、非嵌套。
  • 路由树用 addChildren 手动拼,父子关系要和 getParentRoute 一致。
  • 虚拟文件路由用 @tanstack/virtual-file-routes,在文件式和代码式之间找平衡:physical 在虚拟路由里嵌文件式,__virtual.ts 在文件式里嵌虚拟路由。
  • 选择上:新项目用文件式,老项目或特殊需求用代码式或虚拟文件路由。

代码式路由虽然不推荐做主力,但理解它能让你更深入掌握 TanStack Router 的工作原理,也能在特殊场景下灵活应对。下一章讲类型安全导航和 Link 组件,这是 TanStack Router 类型安全最直观的体现。