Router 入门:路由概念与安装
本教程共 38 篇 · 第 15 篇 · 更新于 2026-07-27 · 约 12 分钟阅读
15. Router 入门:路由概念与安装
本节目标:搞清楚 TanStack Router 是什么、为什么用它,理解路由树和路由匹配的规则,装好包并跑通一个最小路由应用。学完你能用代码式路由搭出可导航的页面。
15.1 TanStack Router 是个啥
前面 11 章都在讲 TanStack Query,管的是数据。这一章开始换主题,讲路由(Routing)。
路由说白了就是:URL 变了,显示哪个页面。访问 /about 显示关于页,访问 /posts/123 显示第 123 篇文章,这套规则就是路由。React 生态里最有名的路由库是 React Router,但 TanStack Router 凭几个硬实力杀了出来:
- 100% 类型推导:你写
<Link to="/posts/123" />,TypeScript 会自动检查这个路由存不存在、参数对不对。写错一个字母直接红线。 - 类型安全的搜索参数:URL 里
?page=2&sort=desc这串东西,在 TanStack Router 里能被校验、被类型化,改一个参数都有类型提示。 - 内置 loader 缓存:路由切换前先拉数据,自带 SWR 风格的缓存,不用每次切换都重新请求。
- 为客户端数据缓存而生:和 TanStack Query 天然集成,loader 里
prefetchQuery,组件里useQuery,丝滑。 - 文件式 + 代码式双支持:两种路由配置方式能混着用。
它不是 React Router 的替代品那么简单,而是把”类型安全”这件事推到了路由的每个角落。
15.2 路由树是什么
理解 TanStack Router 的关键,是理解路由树(Route Tree)。
URL 和组件不是一一对应的简单映射,而是一棵树。访问 /blog/posts/123,渲染的不是单个组件,而是一棵嵌套的组件树:
<Blog>
<Posts>
<Post postId="123" />
</Posts>
</Blog>
外层 Blog 是布局,里面 Posts 是列表容器,最里层 Post 才是具体文章。这种嵌套结构对应到文件,就是一棵路由树:
/routes
├── __root.tsx
├── index.tsx
├── about.tsx
├── posts/
│ ├── index.tsx
│ ├── $postId.tsx
├── settings/
│ ├── profile.tsx
│ ├── notifications.tsx
整棵树有个根节点 __root.tsx,它包住所有路由。每个文件对应一个路由,文件路径决定 URL 路径。TanStack Router 拿这棵树去匹配 URL,匹配上了就把对应的组件嵌套着渲染出来。
Note路由树可以用文件式(按文件命名约定生成)或代码式(手动
createRoute构建)两种方式。两种方式功能完全一样,文件式代码更少,是官方推荐。代码式更灵活,适合老项目接入。后面第 16、17 章分别讲。
15.3 路由的几种类型
路由树里每个节点是一种路由类型,搞清楚分类才好理解后面的内容。
15.3.1 根路由(Root Route)
整棵树的根,用 createRootRoute() 创建。它没有 path,但永远会被匹配,它的组件永远会渲染。可以理解为整个 App 的外壳,所有页面都套在它里面。
import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => <div>根布局,所有页面都包在这里</div>,
})
如果要在根路由里放上下文(比如 QueryClient),用 createRootRouteWithContext<T>(),第 21 章会用到。
15.3.2 基础路由(Basic Routes)
精确匹配某个路径的路由。/about、/settings/notifications 都是基础路由,路径写死,URL 完全一致才匹配。
15.3.3 索引路由(Index Routes)
父路由被精确匹配、且没有子路由匹配时,显示的路由。文件名以 / 结尾表示索引,比如 posts.index.tsx 对应 /posts/。
15.3.4 动态路由(Dynamic Routes)
路径段以 $ 开头的路由,能捕获 URL 的某一段。比如 posts.$postId.tsx 匹配 /posts/123,params 里就有 { postId: '123' }。$ 可以出现在路径任意一段,/posts/$postId/$revisionId 也行。
15.3.5 通配路由(Splat Routes)
路径只有 $ 的路由,匹配从 $ 开始到结尾的所有内容。files/$.tsx 能匹配 /files/documents/hello-world,捕获的字符串放在 params._splat 里。
Warningv1 里通配路由的参数 key 是
_splat,同时兼容旧的*key,v2 会移除*。新项目直接用_splat。
15.3.6 可选路径参数
用 {-$paramName} 语法定义可选参数。posts.{-$category}.tsx 既能匹配 /posts(category 是 undefined),也能匹配 /posts/tech(category 是 "tech")。
15.3.7 布局路由(Layout Routes)
用来包裹子路由,提供共享布局、loader、错误边界等。文件名是 app.tsx,子路由用 app.dashboard.tsx、app.settings.tsx 这种命名,app.tsx 就成了它们的布局。
// app.tsx
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app')({
component: () => (
<div>
<h1>App 布局</h1>
<Outlet /> {/* 子路由渲染在这里 */}
</div>
),
})
访问 /app/dashboard 时,渲染的是 <AppLayout><Dashboard /></AppLayout>,外层布局套着子页面。
15.3.8 无路径布局路由(Pathless Layout Routes)
下划线 _ 开头的路由,有布局功能但不占 URL 路径段。_pathlessLayout.tsx 包住 _pathlessLayout.a.tsx 和 _pathlessLayout.b.tsx,访问 /a 渲染 <PathlessLayout><A /></PathlessLayout>,URL 里没有 _pathlessLayout 这一段。
适合”一组页面共享布局但不想在 URL 体现”的场景,比如登录前后的布局切换。
15.3.9 非嵌套路由
文件名里用 _ 后缀(注意是后缀,不是前缀)能让路由脱离父级嵌套。posts_.$postId.edit.tsx 虽然名字带 posts,但渲染时不套 <Posts> 组件,直接渲染 <PostEditor>。
15.4 路由匹配规则
路由这么多,URL 来了先匹配谁?TanStack Router 会把所有路由按特异性排序,最具体的优先匹配:
- 索引路由(最优先)
- 静态路由(从长到短)
- 动态路由(从长到短)
- 通配路由(最后)
不管你定义路由的顺序怎么样,最后都按这个规则排序。所以 /posts/featured 会比 /posts/$postId 先匹配,特定路径不会被动态路径抢走。
举个例子,路由树是这样的:
Root
- /
- about
- about/us
- blog
- /
- new
- $postId
- *
访问 /blog/my-post 的匹配过程:
Root
❌ /
❌ about
❌ about/us
⏩ blog
❌ /
❌ new
✅ $postId // 匹配上
访问 /not-a-route 这种没定义的路径,会落到 * 通配路由上,用来做 404 页面。
15.5 安装
15.5.1 用脚手架快速创建
新项目最快的方式是用官方 CLI:
npx @tanstack/cli create --router-only
CLI 会问你几个问题:用文件式还是代码式、要不要 TypeScript、要不要 Tailwind、要不要初始化 Git。一路选完,一个能跑的项目就生成好了。
Tip
--router-only表示只装 Router,不装 Start(Start 是基于 Router 的全栈框架,第 22 章讲)。如果你想直接上全栈,去掉这个参数。
15.5.2 手动装到现有项目
老项目接入,手动装包就行。
前置要求:
- React v18 或更高(要支持
createRoot) - react-dom v18 或更高
- 推荐 TypeScript v5.3+(不是必须,但 TanStack Router 的类型安全是核心卖点,不用 TS 等于白瞎)
安装:
npm install @tanstack/react-router
# 或
pnpm add @tanstack/react-router
# 或
yarn add @tanstack/react-router
装完检查 package.json,能看到 @tanstack/react-router 的版本号就行:
{
"dependencies": {
"@tanstack/react-router": "^1.170.18"
}
}
NoteTanStack Router 当前只支持 React(配 ReactDOM)和 Solid。React Native、Vue、Angular 暂不支持。本教程只讲 React 适配器。
15.6 搭一个最小代码式路由应用
装完包,咱们手搓一个能跑的最小路由,用代码式路由(文件式下一章讲)。这样你能直观看到路由树长啥样。
15.6.1 创建根路由
新建 src/router.tsx,先建根路由:
import {
createRootRoute,
createRoute,
createRouter,
Outlet,
} from '@tanstack/react-router'
// 根路由,所有路由的祖宗
const rootRoute = createRootRoute({
component: () => (
<div>
<nav>
<a href="/">首页</a> | <a href="/about">关于</a>
</nav>
<Outlet />
</div>
),
})
createRootRoute 创建根路由,component 里放个简单的导航和 <Outlet />。Outlet 是子路由渲染的占位符,相当于”子页面塞这里”。
15.6.2 创建子路由
加两个子路由:首页和关于页。
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <h1>这是首页</h1>,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: () => <h1>这是关于页</h1>,
})
每个子路由都要指定 getParentRoute 告诉它爹是谁,path 是匹配的 URL,component 是渲染的内容。
15.6.3 组装路由树
把根路由和子路由拼成一棵树:
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
addChildren 接收一个数组,把子路由挂到父路由下。多层嵌套就一层层 addChildren 套下去。
15.6.4 创建 Router 实例
const router = createRouter({
routeTree,
defaultPreload: 'intent', // 鼠标悬停时预加载,体验更好
})
// 类型注册,让 TypeScript 认识你的路由
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
最后那个 declare module 很重要,它把你的 router 实例的类型注册给 TanStack Router,后面用 <Link to="..."> 时才能享受类型检查。漏了这步,类型安全就废了一半。
15.6.5 挂载到应用
import { RouterProvider } from '@tanstack/react-router'
import ReactDOM from 'react-dom/client'
const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(<RouterProvider router={router} />)
RouterProvider 把 router 实例注入到 React 树,整个应用就能用了。
15.6.6 跑起来
启动开发服务器(npm run dev),访问 / 看到首页,点导航跳到 /about 看到关于页。最小应用就成了。
Warning注意上面导航用的是原生
<a href>,会触发整页刷新。生产里要用<Link>组件(第 18 章讲),这里为了演示最简原理先用<a>。
15.7 和 React Router 对比
很多人会问:我已经在用 React Router,有必要换吗?看张对比表(节选自官方):
| 能力 | TanStack Router | React Router |
|---|---|---|
| 类型安全路由 | 完整支持 | 部分(1/5) |
| 类型安全路径参数 | 支持 | 支持 |
| 类型安全搜索参数 | 支持 | 不支持 |
| 搜索参数 Schema 校验 | 支持 | 不支持 |
| 路由上下文类型安全 | 支持 | 不支持 |
| SWR 风格 loader 缓存 | 支持 | 不支持 |
| 文件式路由 | 支持 | 支持 |
| 代码式路由 | 支持 | 支持 |
| 虚拟/编程式文件路由 | 支持 | 支持 |
| 路由预取 | 支持 | 支持 |
| 预取延迟控制 | 支持 | 需自定义 |
| 导航阻塞(useBlocker) | 支持(含硬刷新/跨源) | 部分(不含硬刷新/跨源) |
| Devtools | 内置 | 社区插件 |
| 搜索参数中间件 | 支持 | 不支持 |
简单总结:React Router 够用、稳定、生态广;TanStack Router 在类型安全和搜索参数管理上甩开对手一截,loader 缓存和与 Query 集成也是亮点。
Tip选型建议:新项目、用 TypeScript、重视类型安全和搜索参数,选 TanStack Router。老项目已经在 React Router 上跑得好好的,没强需求不用折腾。如果是 TanStack Start(第 22 章)项目,Router 是内置的,没得选也不用选。
15.8 常见坑
坑一:忘了 declare module 注册类型。 不注册的话,<Link to="/abc"> 不会检查路由是否存在,等于丢了类型安全。每次创建 router 后都要加这段。
坑二:用 React Router 的思维套 TanStack Router。 React Router 是 <Route path="/" element={<Home />} /> 这种声明式,TanStack Router 是先建路由树再 createRouter。思路不一样,别硬套。
坑三:根路由忘了 <Outlet />。 根路由组件里不写 <Outlet />,子路由的内容渲染不出来,页面一片空白。我第一次写就栽在这。
坑四:动态路由参数取错。 posts.$postId.tsx 的参数 key 是 postId,不是 posts.$postId。取参数用 Route.useParams() 返回 { postId: '123' }。
坑五:CLI 装的包名写错。 网上老文章可能写 @tanstack/router,正确包名是 @tanstack/react-router(React 适配器)。CLI 不会装错,手动装时别抄错。
15.9 小结
这一章你认识了 TanStack Router:
- 一款 100% 类型安全的路由库,类型安全和搜索参数管理是核心卖点。
- 用路由树组织 URL 和组件的嵌套关系,根路由 + 子路由层层挂载。
- 路由分基础、索引、动态、通配、布局、无路径布局等多种类型。
- 路由匹配按特异性排序,索引 > 静态 > 动态 > 通配。
- 装包用
@tanstack/react-router,新项目用 CLI,老项目手动装。 - 代码式路由用
createRootRoute+createRoute+createRouter三步搭起来。
下一章讲文件式路由,让你不用手写路由树,靠文件命名约定自动生成。那是官方推荐的玩法,代码更省。