学习 TanStack Router:从零开始 – wiki基地


深入探索现代前端路由:从零开始学习 TanStack Router

在构建现代单页应用 (SPA) 时,路由是核心组成部分之一。它负责将 URL 映射到应用的不同视图或组件,管理页面之间的导航,以及在某些情况下处理数据加载和状态管理。多年来,前端路由库经历了显著的发展,从早期的 hash 路由到现在的声明式、嵌套、类型安全的路由方案。

在众多流行的路由库中,TanStack Router (前身为 React Router v6 的一个分支项目) 凭借其创新的设计理念、强大的类型安全特性、内置的数据加载能力以及对嵌套路由的深度支持,正迅速成为前端开发者的新宠。与传统的路由库相比,TanStack Router 提供了一种更结构化、更可靠、更高效的方式来构建复杂的应用路由。

本文将带你踏上一段学习 TanStack Router 的旅程,从最基础的概念开始,一步步深入其核心特性,最终让你能够熟练地在自己的 React 应用中使用它构建健壮、高性能的路由系统。无论你之前使用过 React Router、Vue Router 还是其他路由库,本文都将为你提供一个清晰的学习路径。

为什么选择 TanStack Router?它的优势何在?

在开始学习之前,我们先来了解一下为什么选择 TanStack Router 可能会是一个明智的决定。它相对于其他库,尤其是一些老牌库,提供了一些独特的优势:

  1. 卓越的类型安全: 这是 TanStack Router 最引人注目的特性之一。通过使用 TypeScript 和代码生成(或手动类型定义),TanStack Router 可以在编译时捕获许多常见的路由错误,比如错误的路径、不存在的参数、类型不匹配的搜索参数等。这大大减少了运行时错误,提高了开发效率和代码质量。
  2. 强大的数据加载能力: TanStack Router 内置了强大的数据加载(Loader)机制。你可以在路由定义中直接指定数据加载函数,Router 会在路由切换 之前同时 执行这些函数,确保组件在渲染时已经获取到所需的数据。这解决了传统路由中常见的数据瀑布问题(组件渲染后再加载数据),提升了用户体验和应用性能。
  3. 深度嵌套路由支持: TanStack Router 的路由定义是天然嵌套的。每个路由都可以有子路由,形成一个路由树。这与现代组件树结构完美契合,使得路由的定义、布局、数据加载和错误处理都能在对应的层级进行管理,极大地简化了复杂应用的路由结构。
  4. 首屏渲染优化: 结合 Loader 和嵌套路由,TanStack Router 能够智能地并行加载多个层级的数据,优化首屏渲染速度。
  5. 内置的 Pending 和 Error 处理: 路由加载数据时,可以方便地展示加载中 (Pending) 状态。如果加载失败,可以在路由层级捕获并展示错误信息,而不会影响应用的其他部分。
  6. 现代 API 设计: API 设计简洁、声明式,易于理解和使用。与 TanStack 生态系统的其他库(如 React Query/TanStack Query)集成紧密。
  7. 无框架限制 (未来可期): 虽然目前主要用于 React,但 TanStack 的目标是构建框架无关的库,未来可能会更容易地应用于 Vue, Svelte 等其他框架。

总而言之,TanStack Router 不仅仅是一个简单的 URL 匹配库,它提供了一个完整的解决方案,将路由、数据、状态和类型安全紧密结合,帮助开发者构建更健壮、更高效、更易维护的现代前端应用。

Prerequisites (准备工作)

在开始学习之前,你需要准备以下环境:

  1. Node.js 和 npm/yarn/pnpm: 确保你的开发环境中安装了 Node.js (推荐 LTS 版本) 以及任意一个包管理器。
  2. React 项目: 你需要一个现有的 React 项目,或者创建一个新的 React 项目。可以使用 Create React App (虽然已不再推荐用于新项目,但作为入门简单), Vite, Next.js (如果你选择 App Router 可能不需要 TanStack Router, 但 Page Router 可以用), 或其他现代 React 脚手架。本文将以一个基于 Vite 的简单 React 项目为例进行讲解。
  3. 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.tssrc/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.tsxsrc/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 devyarn 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 */}

),
})

rootRoutecomponent 渲染了全局的 Header 和 Footer,并在中间放置了 <Outlet />。这意味着所有以 rootRoute 为父级的顶级路由(如 /, /about)都会在这个 <Outlet /> 的位置渲染它们自己的组件。

现在,假设我们要创建一个 /dashboard 区域,它有自己的侧边栏布局,内部包含 /dashboard/overview/dashboard/settings 等子页面。

  1. 创建 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 …
    ``
    注意:
    dashboardRoutepath‘dashboard’而不是‘/dashboard’。对于非根路由,通常使用相对路径段。/dashboard的完整路径是由其父级 (rootRoute/) 和它自己的path` (‘dashboard’) 组合而成。

  2. 创建 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*和*dashboardIndexRoutedashboardRoutecomponent会渲染,其中的将渲染dashboardIndexRoutecomponent
    *
    dashboardSettingsRoute:path: ‘settings’表示相对于父路由dashboardRoute(/dashboard) 的路径/dashboard/settings`。

  3. 更新路由树:

    现在需要将新的路由添加到 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 路由来显示单篇文章。

  1. 定义带有参数的路由:

    “`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 …
    “`

  2. 更新路由树:

    “`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,它们可能会并行执行,或者按照依赖关系执行。

  1. 在路由中定义 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 会先执行 postRouteloader 函数。如果成功,获取到文章数据并渲染组件;如果 fetchPostById 抛出错误,会渲染 errorComponent。在数据加载过程中,如果定义了 pendingComponent,则会显示加载提示。

这种模式非常强大,它将数据获取逻辑从组件中分离出来,与路由紧密耦合,提供了更好的性能和更清晰的代码结构。

Step 9: 类型注册和代码生成 (让类型安全发光)

虽然我们在前面的例子中使用了 route.useParams()route.useLoaderData() 来获取基本的类型推断,但要实现全面的类型安全,特别是对路由路径、参数和搜索参数的编译时检查,强烈推荐使用 TanStack Router 的类型注册机制或代码生成工具。

方法 1: 手动类型注册

在你的全局 .d.ts 文件中(例如 src/tanstack-router.d.tssrc/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 工具,可以根据你的路由文件自动生成类型定义。这是官方推荐的方式,可以确保类型定义始终与你的路由结构同步。

  1. 安装 CLI 工具:

    “`bash
    npm install @tanstack/router-cli -D

    or yarn add @tanstack/router-cli -D

    or pnpm add @tanstack/router-cli -D

    “`

  2. 配置 router.config.json:
    在项目根目录下创建 router.config.json 文件:

    json
    // router.config.json
    {
    "routesDirectory": "./src/routes.tsx", // 或你的路由文件路径
    "generatedRouteTreeFile": "./src/routeTree.gen.ts" // 生成的文件路径
    }

  3. 运行代码生成命令:
    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 文件。

  4. 在你的路由文件中使用生成的文件:
    修改 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 tree

    export const router = createRouter({ routeTree })

    // 5. 不需要手动类型注册了,生成的文件已经做了
    “`

    重要: 确保 src/routeTree.gen.ts 被你的 TypeScript 项目包含(通常默认会包含)。你可能需要在开发服务器启动时或在 CI/CD 流程中运行 npm run generate-router。可以考虑使用工具如 concurrentlynodemon 来在开发模式下监视路由文件变化并自动重新生成。

使用代码生成后,Linkto 属性将具有完整的路由路径、参数和搜索参数的类型提示和检查。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 和类型安全),但一旦掌握,你会发现它能显著提升开发效率和应用质量。

下一步的学习建议:

  1. 实践: 动手在你自己的项目中实践本文中学到的概念,构建一个包含嵌套路由、参数、搜索参数和 Loader 的小应用。
  2. 探索高级特性: 阅读官方文档,深入了解 beforeLoad, Context, Lazy Loading 等高级特性。
  3. 结合数据管理库: 将 TanStack Router 的 Loader 与 TanStack Query 或 SWR 等数据获取库结合使用,体验更顺畅的数据流管理。
  4. 尝试文件路由: 如果你的项目结构允许,尝试使用文件路由插件来简化路由定义。
  5. 阅读源码和示例: 查看 TanStack Router 的官方示例或源码,学习更复杂的用法和最佳实践。

路由是前端应用的地基。掌握 TanStack Router 这样现代化的路由方案,将为你构建复杂、高性能、易维护的应用打下坚实的基础。祝你在学习和使用 TanStack Router 的旅程中一切顺利!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部