深入探索现代前端路由:从零开始学习 TanStack Router
在构建现代单页应用 (SPA) 时,路由是核心组成部分之一。它负责将 URL 映射到应用的不同视图或组件,管理页面之间的导航,以及在某些情况下处理数据加载和状态管理。多年来,前端路由库经历了显著的发展,从早期的 hash 路由到现在的声明式、嵌套、类型安全的路由方案。
在众多流行的路由库中,TanStack Router (前身为 React Router v6 的一个分支项目) 凭借其创新的设计理念、强大的类型安全特性、内置的数据加载能力以及对嵌套路由的深度支持,正迅速成为前端开发者的新宠。与传统的路由库相比,TanStack Router 提供了一种更结构化、更可靠、更高效的方式来构建复杂的应用路由。
本文将带你踏上一段学习 TanStack Router 的旅程,从最基础的概念开始,一步步深入其核心特性,最终让你能够熟练地在自己的 React 应用中使用它构建健壮、高性能的路由系统。无论你之前使用过 React Router、Vue Router 还是其他路由库,本文都将为你提供一个清晰的学习路径。
为什么选择 TanStack Router?它的优势何在?
在开始学习之前,我们先来了解一下为什么选择 TanStack Router 可能会是一个明智的决定。它相对于其他库,尤其是一些老牌库,提供了一些独特的优势:
- 卓越的类型安全: 这是 TanStack Router 最引人注目的特性之一。通过使用 TypeScript 和代码生成(或手动类型定义),TanStack Router 可以在编译时捕获许多常见的路由错误,比如错误的路径、不存在的参数、类型不匹配的搜索参数等。这大大减少了运行时错误,提高了开发效率和代码质量。
- 强大的数据加载能力: TanStack Router 内置了强大的数据加载(Loader)机制。你可以在路由定义中直接指定数据加载函数,Router 会在路由切换 之前 或 同时 执行这些函数,确保组件在渲染时已经获取到所需的数据。这解决了传统路由中常见的数据瀑布问题(组件渲染后再加载数据),提升了用户体验和应用性能。
- 深度嵌套路由支持: TanStack Router 的路由定义是天然嵌套的。每个路由都可以有子路由,形成一个路由树。这与现代组件树结构完美契合,使得路由的定义、布局、数据加载和错误处理都能在对应的层级进行管理,极大地简化了复杂应用的路由结构。
- 首屏渲染优化: 结合 Loader 和嵌套路由,TanStack Router 能够智能地并行加载多个层级的数据,优化首屏渲染速度。
- 内置的 Pending 和 Error 处理: 路由加载数据时,可以方便地展示加载中 (Pending) 状态。如果加载失败,可以在路由层级捕获并展示错误信息,而不会影响应用的其他部分。
- 现代 API 设计: API 设计简洁、声明式,易于理解和使用。与 TanStack 生态系统的其他库(如 React Query/TanStack Query)集成紧密。
- 无框架限制 (未来可期): 虽然目前主要用于 React,但 TanStack 的目标是构建框架无关的库,未来可能会更容易地应用于 Vue, Svelte 等其他框架。
总而言之,TanStack Router 不仅仅是一个简单的 URL 匹配库,它提供了一个完整的解决方案,将路由、数据、状态和类型安全紧密结合,帮助开发者构建更健壮、更高效、更易维护的现代前端应用。
Prerequisites (准备工作)
在开始学习之前,你需要准备以下环境:
- Node.js 和 npm/yarn/pnpm: 确保你的开发环境中安装了 Node.js (推荐 LTS 版本) 以及任意一个包管理器。
- React 项目: 你需要一个现有的 React 项目,或者创建一个新的 React 项目。可以使用 Create React App (虽然已不再推荐用于新项目,但作为入门简单), Vite, Next.js (如果你选择 App Router 可能不需要 TanStack Router, 但 Page Router 可以用), 或其他现代 React 脚手架。本文将以一个基于 Vite 的简单 React 项目为例进行讲解。
- TypeScript (强烈推荐): 虽然 TanStack Router 可以在 JavaScript 项目中使用,但其最大的优势之一在于类型安全。强烈建议你在一个 TypeScript 项目中学习和使用它,这样才能充分体验其带来的便利。
假设你已经有一个基础的 React + TypeScript 项目。
Step 1: 安装 TanStack Router
首先,你需要将 TanStack Router 安装到你的项目中。根据你使用的包管理器执行以下命令:
“`bash
使用 npm
npm install @tanstack/react-router history
使用 yarn
yarn add @tanstack/react-router history
使用 pnpm
pnpm add @tanstack/react-router history
“`
@tanstack/react-router
: 这是 TanStack Router 的核心 React 绑定库。history
: TanStack Router 内部依赖于 history 库来管理浏览器历史堆栈。
Step 2: 创建你的第一个 Router
TanStack Router 的核心是 Router
实例。你需要创建一个根路由 (rootRoute
),然后围绕它构建你的路由树,最后创建 router
实例,并将其提供给你的应用。
我们通常在一个单独的文件中定义路由,例如 src/routeTree.ts
或 src/routes.tsx
。
创建一个新文件,比如 src/routes.tsx
:
“`tsx
// src/routes.tsx
import { createRootRoute, createRoute, createRouter } from ‘@tanstack/react-router’
import App from ‘./App’ // 假设你的主应用组件在 App.tsx
// 1. 定义根路由 (Root Route)
// 根路由不对应任何特定的路径,但它是整个路由树的起点
// 它通常用于放置全局的布局(如 Header/Footer)或者 Context Provider
const rootRoute = createRootRoute({
component: () => (
<>
{/ 可以在这里放置全局导航或布局 /}
Home
{‘ ‘}
About
{/ Outlet 是嵌套路由的关键,它会渲染当前匹配到的子路由的组件 /}
{/ 可以在这里放置全局 Footer /}
),
})
// 2. 定义各个页面的路由
// 创建 Home 页面路由
const indexRoute = createRoute({
getParentRoute: () => rootRoute, // 指定父路由为根路由
path: ‘/’, // 路径为根路径 ‘/’
component: () => (
Welcome Home!
),
})
// 创建 About 页面路由
const aboutRoute = createRoute({
getParentRoute: () => rootRoute, // 指定父路由为根路由
path: ‘/about’, // 路径为 ‘/about’
component: () => (
About Page
This is the about page.
),
})
// 3. 构建路由树 (Route Tree)
// 将所有顶级路由作为根路由的 children 数组
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
])
// 4. 创建 Router 实例
export const router = createRouter({ routeTree })
// 5. 为 Router 实例添加类型定义 (可选但强烈推荐)
// 确保你的 tsconfig.json 中设置了 allowSyntheticDefaultImports: true
// 在你的 index.d.ts 或 env.d.ts 文件中添加以下内容:
/
declare module ‘@tanstack/react-router’ {
interface Register {
router: typeof router
}
}
/
// 或者使用代码生成工具 (推荐,后面会讲到)
“`
代码解释:
createRootRoute()
: 创建路由树的根节点。它没有path
属性,作为所有顶级路由的父级。createRoute({})
: 创建一个具体的路由定义。getParentRoute: () => rootRoute
: 指定当前路由的父级。所有顶级路由都以rootRoute
为父级。path: '/'
,path: '/about'
: 定义路由的 URL 路径。component: () => (...)
: 指定当这个路由匹配时应该渲染的 React 组件。
rootRoute.addChildren([...])
: 将定义的子路由添加到父路由(这里是根路由)下,构建出路由树的结构。createRouter({ routeTree })
: 使用构建好的路由树创建一个router
实例。Outlet
: 这是 TanStack Router 提供的一个组件,必须放在父路由的component
中。它指示了子路由的组件应该在哪里渲染。这是一个非常重要的概念,是实现嵌套布局的关键。Link
: 也是 TanStack Router 提供的组件,用于创建导航链接。与<a>
标签不同,它会阻止页面刷新,通过 history API 进行客户端路由跳转。
Step 3: 将 RouterProvider 添加到应用
在你的应用入口文件(通常是 src/main.tsx
或 src/index.tsx
)中,你需要引入创建的 router
实例,并使用 RouterProvider
组件包裹你的应用。
“`tsx
// src/main.tsx
import React from ‘react’
import ReactDOM from ‘react-dom/client’
// import App from ‘./App.tsx’ // 不再直接渲染 App
import ‘./index.css’
import { RouterProvider } from ‘@tanstack/react-router’
import { router } from ‘./routes’ // 引入你的 router 实例
ReactDOM.createRoot(document.getElementById(‘root’)!).render(
{/ 使用 RouterProvider 包裹你的应用 /}
{/ router={router} 将你创建的 router 实例传递给 Provider /}
)
“`
现在,当你运行你的应用时 (npm run dev
或 yarn dev
),你应该能看到基础的导航链接和对应页面的内容。尝试点击链接,你会看到 URL 变化,但页面没有刷新。
Step 4: Link 组件和导航
Link
组件是 TanStack Router 中用于导航的标准方式。
“`tsx
import { Link } from ‘@tanstack/react-router’
// 基本用法
Home
About
// 嵌套路径 (相对路径) – 假设当前在 /posts
// 链接到 /posts/1
View Post 1
// 链接到 /posts/new
Create New Post // ‘../’ 表示回到父路径
// 传递参数和搜索参数
User 123
Posts (Desc, Page 2)
// 高亮当前活动链接 (自动添加 active 类)
About
// 传递 state (可以通过 Route.useRouteContext() 或 useRouterState() 获取)
Item 1 (from list)
“`
Link
组件的强大之处:
- 自动处理导航: 它会阻止默认的
<a>
行为,使用 history API 进行单页应用导航。 - 类型安全: 如果你使用了类型生成或手动类型定义,
to
属性的值会被检查,确保路径、参数和搜索参数的类型和结构正确。 - 相对路径: 在嵌套路由中,可以使用相对路径 (
../
,./
),这使得路由定义更加模块化。 - 活动类: 自动为当前匹配的链接添加
active
类,方便高亮样式。
编程式导航:
除了 Link
组件,你还可以通过代码进行导航,例如在表单提交后或者某个事件触发时。可以使用 useNavigate()
hook 或者直接访问 router
实例。
“`tsx
import { useNavigate, useRouter } from ‘@tanstack/react-router’
function MyComponent() {
const navigate = useNavigate() // Hook 方式
const router = useRouter() // 获取 router 实例方式
const handleLogin = async () => {
// … 执行登录逻辑 …
const success = true; // 假设登录成功
if (success) {
// 导航到仪表盘
navigate({ to: '/dashboard' })
// 或者 router.navigate({ to: '/dashboard' })
// 导航并替换历史记录 (不会创建新的历史条目)
// navigate({ to: '/dashboard', replace: true })
// 导航到带参数和搜索参数的路径
// navigate({
// to: '/users/$userId',
// params: { userId: 456 },
// search: { tab: 'profile' },
// })
// 导航到外部 URL
// navigate({ to: 'https://google.com', external: true })
}
}
return (
)
}
“`
navigate()
函数的选项 (to
, params
, search
, replace
, state
, external
等) 提供了灵活的导航控制。同样,这些选项也是类型安全的。
Step 5: 定义嵌套路由和布局
嵌套路由是 TanStack Router 的核心特性之一。它允许你将复杂的 UI 结构分解成可管理的路由片段,并且天然支持嵌套布局。
回顾 Step 2 中的根路由:
tsx
const rootRoute = createRootRoute({
component: () => (
<>
<div className="p-2 flex gap-2">...</div> {/* Global Header */}
<hr />
<Outlet /> {/* <Outlet /> 是关键 */}
{/* Global Footer */}
),
})
rootRoute
的 component
渲染了全局的 Header 和 Footer,并在中间放置了 <Outlet />
。这意味着所有以 rootRoute
为父级的顶级路由(如 /
, /about
)都会在这个 <Outlet />
的位置渲染它们自己的组件。
现在,假设我们要创建一个 /dashboard
区域,它有自己的侧边栏布局,内部包含 /dashboard/overview
和 /dashboard/settings
等子页面。
-
创建 Dashboard 布局路由:
“`tsx
// src/routes.tsx (继续添加)
import { Outlet, Link } from ‘@tanstack/react-router’ // 确保引入 Outlet 和 Link// … previous routes (rootRoute, indexRoute, aboutRoute) …
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute, // Dashboard 是顶级路由,父级是根路由
path: ‘dashboard’, // 路径是 ‘/dashboard’
// Dashboard 布局组件
component: () => ({/ Dashboard 侧边栏 /}Dashboard Nav
- Overview {/ Link 到 Dashboard 首页 /}
- Settings
{/ 更多 dashboard 链接 /}
{/ Dashboard 内容区域,子路由组件将在这里渲染 /}
{/ Dashboard 子路由的组件将在这里渲染 /} ),
})// … add dashboardRoute to the routeTree later …
``
dashboardRoute
注意:的
path是
‘dashboard’而不是
‘/dashboard’。对于非根路由,通常使用相对路径段。
/dashboard的完整路径是由其父级 (
rootRoute的
/) 和它自己的
path` (‘dashboard’) 组合而成。 -
创建 Dashboard 的子路由:
“`tsx
// src/routes.tsx (继续添加)// … previous routes (rootRoute, indexRoute, aboutRoute, dashboardRoute) …
// Dashboard Overview 路由 (作为 Dashboard 的首页)
const dashboardIndexRoute = createRoute({
getParentRoute: () => dashboardRoute, // 父级是 dashboardRoute
path: ‘/’, // 路径是 ‘/’,当访问 ‘/dashboard’ 时匹配
component: () => (Dashboard Overview
Welcome to your dashboard.
),
})// Dashboard Settings 路由
const dashboardSettingsRoute = createRoute({
getParentRoute: () => dashboardRoute, // 父级是 dashboardRoute
path: ‘settings’, // 路径是 ‘settings’,当访问 ‘/dashboard/settings’ 时匹配
component: () => (Dashboard Settings
Manage your settings here.
),
})// … add dashboardIndexRoute and dashboardSettingsRoute to the routeTree later …
``
dashboardIndexRoute
*:
path: ‘/’在嵌套路由中表示父路由的默认页面。当 URL 是
/dashboard时,它会匹配
dashboardRoute*和*
dashboardIndexRoute。
dashboardRoute的
component会渲染,其中的
将渲染
dashboardIndexRoute的
component。
dashboardSettingsRoute
*:
path: ‘settings’表示相对于父路由
dashboardRoute(
/dashboard) 的路径
/dashboard/settings`。 -
更新路由树:
现在需要将新的路由添加到
routeTree
中。“`tsx
// src/routes.tsx (更新 routeTree)// … previous route definitions …
// 构建路由树 (Route Tree)
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
dashboardRoute.addChildren([ // 将 Dashboard 的子路由添加到 dashboardRoute 下
dashboardIndexRoute,
dashboardSettingsRoute,
]),
])// … rest of the file (createRouter, type registration) …
“`
现在,你的路由结构是这样的:
/ (rootRoute)
├── / (indexRoute)
├── /about (aboutRoute)
└── /dashboard (dashboardRoute)
├── / (dashboardIndexRoute) -> Full path: /dashboard
└── /settings (dashboardSettingsRoute) -> Full path: /dashboard/settings
当访问 /dashboard
时,rootRoute
的组件会渲染(显示全局 Header/Footer),在 <Outlet />
位置渲染 dashboardRoute
的组件(显示侧边栏和主内容区),然后在 dashboardRoute
组件中的 <Outlet />
位置渲染 dashboardIndexRoute
的组件(显示 Overview 内容)。
当访问 /dashboard/settings
时,rootRoute
组件 -> dashboardRoute
组件 -> dashboardSettingsRoute
组件,层层嵌套渲染。
这种结构清晰地分离了不同层级的布局和内容,是构建大型应用的强大模式。
Step 6: 处理路由参数 ($param
)
许多路由需要动态的部分,例如用户 ID (/users/123
) 或文章 slug (/blog/tanstack-router-guide
)。TanStack Router 使用 $
前缀来定义路由参数,并且通过 Hook useParams()
提供了类型安全的访问方式。
假设我们要创建 /posts/:postId
路由来显示单篇文章。
-
定义带有参数的路由:
“`tsx
// src/routes.tsx (继续添加)// … previous route definitions …
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: ‘posts’, // ‘/posts’ 路径,作为单篇文章列表和单篇文章的父级
})const postRoute = createRoute({
getParentRoute: () => postsRoute, // 父级是 postsRoute
path: ‘$postId’, // 定义一个路由参数 postId,以 ‘$’ 开头
component: () => {
const { postId } = postRoute.useParams() // 使用 route.useParams() 获取参数,类型安全!
// 注意这里使用 postRoute.useParams() 而不是 useParams()。
// 这是为了更好的类型推断。每个 createRoute 调用都会返回一个带有与其参数类型绑定的 use hooks 的对象。
return (Post Detail
Viewing post with ID: {postId}
{/ 在这里加载并显示具体的文章内容 */}
)
},
})// Optional: A route for /posts itself (e.g., a list of posts)
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: ‘/’, // /posts 的首页
component: () => (Posts List
- Post 1
- Post 2
)
})// … update routeTree …
“` -
更新路由树:
“`tsx
// src/routes.tsx (更新 routeTree)// … previous route definitions …
// 构建路由树 (Route Tree)
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
dashboardRoute.addChildren([
dashboardIndexRoute,
dashboardSettingsRoute,
]),
postsRoute.addChildren([ // 添加 postsRoute 及其子路由
postsIndexRoute, // Add the list route
postRoute, // Add the detail route with parameter
]),
])// … rest of the file …
“`
现在你可以通过 /posts/1
或 /posts/abc
这样的 URL 访问该路由,并在 postRoute
的组件中使用 postRoute.useParams()
获取到 postId
的值。由于使用了 $postId
命名约定,TypeScript 可以推断出 useParams()
返回的对象中有一个 postId
属性。
Step 7: 处理搜索参数 (useSearch()
)
除了路径参数,URL 还经常包含搜索参数(Query Parameters),例如 /users?sort=name&page=2
。TanStack Router 提供了 useSearch()
hook 来方便地访问和验证这些参数。
“`tsx
import { createRoute } from ‘@tanstack/react-router’
// … previous routes …
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: ‘users’,
// 使用 validateSearch 选项来定义和验证搜索参数的类型和默认值
// 这是一个可选但强烈推荐的步骤,提供了强大的类型安全和数据解析
validateSearch: (search: Record
// Zod 是一个流行的 schema 验证库,这里用它举例
// 你也可以手动进行类型检查和转换
return {
page: Number(search.page ?? 0), // 默认 page=0
pageSize: Number(search.pageSize ?? 10), // 默认 pageSize=10
sort: search.sort as ‘asc’ | ‘desc’ | undefined, // 允许 ‘asc’, ‘desc’ 或 undefined
}
},
component: () => {
// 使用 usersRoute.useSearch() 获取类型安全的搜索参数
const { page, pageSize, sort } = usersRoute.useSearch()
return (
<div className="p-2">
<h5>Users List</h5>
<p>Current Page: {page}</p>
<p>Page Size: {pageSize}</p>
<p>Sort By: {sort ?? 'Default'}</p>
{/* 在这里加载并显示用户列表 */}
</div>
)
},
})
// … update routeTree …
const routeTree = rootRoute.addChildren([
// … other routes …
usersRoute, // Add the users route
]);
// … rest …
“`
validateSearch
: 这是一个强大的选项,用于定义预期的搜索参数结构、类型以及如何解析/验证它们。它接收原始的搜索参数对象Record<string, unknown>
,并应该返回一个经过验证和类型化的对象。这确保了你在组件中通过useSearch()
获取到的数据是可靠的。route.useSearch()
: 返回经过validateSearch
处理后的搜索参数对象。
现在你可以通过 /users?page=1&sort=desc
这样的 URL 访问,并在组件中安全地使用 page
, pageSize
, sort
这些变量,并且它们的类型是已知的。
Step 8: 使用 Loader 进行数据加载 (核心特性!)
这是 TanStack Router 最重要的特性之一。Loader 函数允许你在路由进入 之前 或 同时 异步加载数据。这与传统的在组件 useEffect
中加载数据的方式不同,避免了先渲染加载状态再获取数据的“瀑布流”问题,提高了用户体验和性能。
Loader 函数在路由匹配后、组件渲染前执行。如果父路由和子路由都有 Loader,它们可能会并行执行,或者按照依赖关系执行。
-
在路由中定义 Loader:
“`tsx
// src/routes.tsx (继续修改 postRoute)// … previous route definitions …
// 模拟一个异步数据获取函数
async function fetchPostById(postId: string) {
console.log(Fetching post ${postId}...
);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500));
// 模拟一些文章数据
const posts = {
‘1’: { id: ‘1’, title: ‘First Post’, body: ‘This is the first post.’ },
‘2’: { id: ‘2’, title: ‘Second Post’, body: ‘This is the second post.’ },
‘abc’: { id: ‘abc’, title: ‘Another Post’, body: ‘Content for another post.’ },
};
const post = posts[postId as keyof typeof posts];
if (!post) {
// 如果找不到,抛出错误,会被 route 的 errorComponent 捕获
throw new Error(Post with id "${postId}" not found
);
}
console.log(Post ${postId} fetched!
);
return post;
}const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: ‘$postId’,
// 定义 loader 函数
loader: async ({ params, search, context }) => {
// loader 函数接收一个对象,包含当前路由的参数、搜索参数和上下文
console.log(‘Loader running for postRoute’, params.postId);
const post = await fetchPostById(params.postId); // 使用路由参数获取数据
return { post }; // loader 函数应该返回一个对象,这个对象将作为数据提供给组件
},
// component 将在 loader 成功执行后渲染
component: () => {
// 使用 route.useLoaderData() 获取 loader 返回的数据
// postRoute.useLoaderData() 的返回值是 { post: PostType },这是类型安全的
const { post } = postRoute.useLoaderData();return ( <div className="p-2"> <h5>{post.title}</h5> <p>{post.body}</p> </div> );
},
// 可选:定义加载中的组件
pendingComponent: () => (Loading post…),
// 可选:定义错误组件
errorComponent: ({ error }) => ( // error 对象包含了 loader 抛出的错误Error loading post:
{error.message}
),
});// … keep postRoute in routeTree …
“`
代码解释:
loader: async ({ params, search, context }) => { ... }
: 这是一个异步函数,在路由匹配时执行。params
: 包含当前路由的路径参数(如$postId
)。search
: 包含当前路由的搜索参数。context
: 可以用于在路由树中传递数据或服务(高级用法)。- Loader 函数应该返回一个任何类型的值(通常是一个对象),这个值将被提供给组件。
route.useLoaderData()
: 在路由的component
中使用这个 Hook 来获取loader
函数返回的数据。这个 Hook 也是类型安全的,你知道它返回的数据结构。pendingComponent
: 如果loader
函数正在执行(等待异步结果),并且没有定义pendingComponent
,路由会什么都不渲染或者渲染父级的 Pending 组件。定义pendingComponent
会在该位置显示一个加载指示器。errorComponent
: 如果loader
函数抛出了错误,路由会渲染这个组件来显示错误信息。这提供了一种局部错误处理机制。
现在,当你访问 /posts/1
或 /posts/invalid-id
时,TanStack Router 会先执行 postRoute
的 loader
函数。如果成功,获取到文章数据并渲染组件;如果 fetchPostById
抛出错误,会渲染 errorComponent
。在数据加载过程中,如果定义了 pendingComponent
,则会显示加载提示。
这种模式非常强大,它将数据获取逻辑从组件中分离出来,与路由紧密耦合,提供了更好的性能和更清晰的代码结构。
Step 9: 类型注册和代码生成 (让类型安全发光)
虽然我们在前面的例子中使用了 route.useParams()
和 route.useLoaderData()
来获取基本的类型推断,但要实现全面的类型安全,特别是对路由路径、参数和搜索参数的编译时检查,强烈推荐使用 TanStack Router 的类型注册机制或代码生成工具。
方法 1: 手动类型注册
在你的全局 .d.ts
文件中(例如 src/tanstack-router.d.ts
或 src/env.d.ts
),添加以下内容:
“`typescript
// src/tanstack-router.d.ts
import { router } from ‘./routes’ // 确保路径正确
declare module ‘@tanstack/react-router’ {
interface Register {
router: typeof router
}
}
“`
这个声明告诉 TanStack Router 的模块你创建的 router
实例的类型是什么。一旦注册,Link
组件的 to
属性、navigate
函数的参数等都会获得更好的类型检查和自动补全。例如,当你输入 <Link to="/posts/">
时,TypeScript 会提示你 /posts
有一个子路由 $postId
,并且 params
是必需的,其类型是 { postId: string }
。
方法 2: 代码生成 (推荐!)
手动注册虽然简单,但随着路由增多,维护类型可能会变得复杂。TanStack Router 提供了一个 CLI 工具,可以根据你的路由文件自动生成类型定义。这是官方推荐的方式,可以确保类型定义始终与你的路由结构同步。
-
安装 CLI 工具:
“`bash
npm install @tanstack/router-cli -Dor yarn add @tanstack/router-cli -D
or pnpm add @tanstack/router-cli -D
“`
-
配置
router.config.json
:
在项目根目录下创建router.config.json
文件:json
// router.config.json
{
"routesDirectory": "./src/routes.tsx", // 或你的路由文件路径
"generatedRouteTreeFile": "./src/routeTree.gen.ts" // 生成的文件路径
} -
运行代码生成命令:
在package.json
中添加一个脚本:json
// package.json
{
// ... other scripts
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"generate-router": "npx @tanstack/router-cli generate" // 添加这个脚本
},
// ...
}
现在,运行npm run generate-router
(或yarn generate-router
,pnpm generate-router
)。这会生成一个src/routeTree.gen.ts
文件。 -
在你的路由文件中使用生成的文件:
修改src/routes.tsx
(或你的路由文件),不再手动构建routeTree
,而是从生成的文件中导入。“`tsx
// src/routes.tsx
import { createRootRoute, createRoute } from ‘@tanstack/react-router’
// import { routeTree } from ‘./routeTree.gen.ts’ // 从生成的文件导入 routeTree// 1. 定义根路由 (Root Route) – 仍然在这里定义
const rootRoute = createRootRoute({
component: () => (
<>
{/ Global Layout /}Home
About
Posts
Dashboard
Users {/ Add Users link /}
),
})// 2. 定义各个页面的路由 – 仍然在这里定义,但不需要手动组合 routeTree
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: ‘/’, component: () =>…Home…})
const aboutRoute = createRoute({ getParentRoute: () => rootRoute, path: ‘/about’, component: () =>…About…})
const dashboardRoute = createRoute({ getParentRoute: () => rootRoute, path: ‘dashboard’, component: () =>
…Dashboard Layout…})
const dashboardIndexRoute = createRoute({ getParentRoute: () => dashboardRoute, path: ‘/’, component: () =>…Dashboard Overview…})
const dashboardSettingsRoute = createRoute({ getParentRoute: () => dashboardRoute, path: ‘settings’, component: () =>…Dashboard Settings…})
const postsRoute = createRoute({ getParentRoute: () => rootRoute, path: ‘posts’ }) // posts parent route
const postsIndexRoute = createRoute({ getParentRoute: () => postsRoute, path: ‘/’, component: () =>…Posts List…Post 1}) // posts list
const postRoute = createRoute({ // post detail
getParentRoute: () => postsRoute,
path: ‘$postId’,
loader: async ({ params }) => fetchPostById(params.postId),
component: () => { const { post } = postRoute.useLoaderData(); return{post.title}
{post.body}
},
pendingComponent: () =>Loading post…,
errorComponent: ({ error }) =>Error: {error.message},
})const usersRoute = createRoute({ // users list with search params
getParentRoute: () => rootRoute,
path: ‘users’,
validateSearch: (search: any) => ({ page: Number(search.page ?? 0), pageSize: Number(search.pageSize ?? 10), sort: search.sort }), // Simplified validation for example
component: () => { const { page, pageSize, sort } = usersRoute.useSearch(); return…Users List…Page: {page}, Size: {pageSize}, Sort: {sort}},
})// 3. 导出所有路由定义,供代码生成使用
export {
rootRoute,
indexRoute,
aboutRoute,
dashboardRoute,
dashboardIndexRoute,
dashboardSettingsRoute,
postsRoute,
postsIndexRoute,
postRoute,
usersRoute,
}// 4. 从生成的文件导入路由树并创建 router 实例
import { routeTree } from ‘./routeTree.gen’ // Import the generated route treeexport const router = createRouter({ routeTree })
// 5. 不需要手动类型注册了,生成的文件已经做了
“`重要: 确保
src/routeTree.gen.ts
被你的 TypeScript 项目包含(通常默认会包含)。你可能需要在开发服务器启动时或在 CI/CD 流程中运行npm run generate-router
。可以考虑使用工具如concurrently
或nodemon
来在开发模式下监视路由文件变化并自动重新生成。
使用代码生成后,Link
的 to
属性将具有完整的路由路径、参数和搜索参数的类型提示和检查。useParams
, useSearch
, useLoaderData
等 Hook 的返回值类型也将根据你的路由定义和 validateSearch
/loader
函数的返回值类型自动推断,极大地提升了开发体验和代码的可靠性。
Step 10: 高级特性简述
学习完以上核心概念,你已经具备了使用 TanStack Router 构建中大型应用路由的基础。TanStack Router 还有一些其他高级特性值得探索:
- Authentication & Authorization (
beforeLoad
): 在路由加载前执行检查,例如用户是否登录。可以使用beforeLoad
Hook 来实现,如果检查失败,可以抛出错误或重定向到登录页。 - Route Context (
useRouteContext
): 在路由树中传递数据或服务,例如认证信息、API 客户端实例等,供 Loader 或组件使用。 - Lazy Loading (
component: () => import(...)
): 使用动态导入实现路由级别的代码分割,按需加载组件,优化应用体积和加载速度。 - ErrorBoundary (
errorComponent
): 路由层级的错误边界,当子路由或 Loader 抛出错误时,可以在父级路由捕获并展示错误 UI。 - Mutations & Invalidation: TanStack Router 的 Loader 可以与 TanStack Query (React Query) 等数据管理库结合,实现数据修改后自动让相关 Loader 失效并重新加载数据。
- File-Based Routing Plugin: 如果你喜欢基于文件结构的路由定义方式(类似 Next.js App Router 或 Remix),TanStack Router 提供了一个插件,可以扫描文件系统自动生成路由树。
总结与下一步
恭喜你!通过本文,你已经从零开始了解了 TanStack Router 的核心概念和使用方法:
- 认识了 TanStack Router 的优势,特别是类型安全和数据加载。
- 学会了如何安装和初始化 Router。
- 掌握了如何定义根路由、基本路由和嵌套路由,以及构建路由树。
- 学会了使用
Link
组件和navigate
函数进行导航。 - 理解了
Outlet
在嵌套布局中的作用。 - 学会了如何定义和使用路由参数 (
$param
) 和搜索参数 (useSearch()
)。 - 深入学习了强大的 Loader 机制,如何在路由加载前获取数据,以及如何处理 Pending 和 Error 状态。
- 了解了类型安全的重要性,以及如何通过手动注册或代码生成来增强类型体验。
TanStack Router 是一个强大且现代的路由库,它强制你以一种结构化、类型安全的方式思考应用路由和数据流。虽然初学时可能需要适应一些新概念(尤其是 Loader 和类型安全),但一旦掌握,你会发现它能显著提升开发效率和应用质量。
下一步的学习建议:
- 实践: 动手在你自己的项目中实践本文中学到的概念,构建一个包含嵌套路由、参数、搜索参数和 Loader 的小应用。
- 探索高级特性: 阅读官方文档,深入了解
beforeLoad
, Context, Lazy Loading 等高级特性。 - 结合数据管理库: 将 TanStack Router 的 Loader 与 TanStack Query 或 SWR 等数据获取库结合使用,体验更顺畅的数据流管理。
- 尝试文件路由: 如果你的项目结构允许,尝试使用文件路由插件来简化路由定义。
- 阅读源码和示例: 查看 TanStack Router 的官方示例或源码,学习更复杂的用法和最佳实践。
路由是前端应用的地基。掌握 TanStack Router 这样现代化的路由方案,将为你构建复杂、高性能、易维护的应用打下坚实的基础。祝你在学习和使用 TanStack Router 的旅程中一切顺利!