TanStack Router 入门指南:轻松玩转现代声明式路由 – wiki基地


TanStack Router 入门指南:轻松玩转现代声明式路由

引言:告别传统,拥抱现代声明式路由

在现代 Web 应用开发中,路由扮演着至关重要的角色。它负责管理应用程序的不同视图(页面)与 URL 之间的映射关系,确保用户能够通过链接导航、刷新页面后状态不丢失,并提供流畅的用户体验。多年来,我们见证了路由解决方案的演变,从基于哈希的路由到历史 API,从命令式配置到声明式组件。

然而,许多现有的路由库在处理复杂应用场景时,仍面临一些挑战:
* 类型安全问题: 在参数传递、搜索查询、路径匹配等方面,缺乏完善的 TypeScript 支持,容易引入运行时错误。
* 数据加载与瀑布效应: 组件在渲染后才开始获取数据,可能导致数据加载延迟、UI 闪烁,甚至出现数据瀑布(waterfall)效应,严重影响用户体验。
* 配置复杂性: 随着路由数量的增加,路由配置变得庞大且难以维护。
* 性能优化不足: 懒加载、预加载等性能特性需要手动配置,不够智能。

正是在这样的背景下,TanStack Router 应运而生。作为 TanStack 系列(包括 TanStack Query, TanStack Table 等)的一员,它继承了 TanStack 生态的哲学:强大、声明式、类型安全、以开发者体验为中心。 TanStack Router 不仅仅是一个路由库,更是一个集成了数据加载、缓存、错误处理和性能优化的全栈路由解决方案。

本文将作为你的 TanStack Router 入门指南,带你深入了解它的核心概念、优势,并通过详尽的代码示例,一步步构建一个现代化的、声明式且高度类型安全的路由系统。准备好了吗?让我们一起踏上这段探索之旅!

第一章:声明式路由的魅力与 TanStack Router 的核心优势

1.1 什么是声明式路由?

在软件开发中,声明式编程与命令式编程是两种截然不同的范式。
* 命令式编程 关注 如何做 (How to do it)。你告诉计算机每一步具体的操作,一步步达到目标。例如,传统 DOM 操作中,你需要写代码来查找元素、创建元素、添加事件监听器等。
* 声明式编程 关注 是什么 (What it is)。你描述你想要达到的最终状态或结果,而具体的执行细节则由框架或库来处理。例如,在 React 中,你描述 UI 的最终状态(JSX),React 会负责高效地更新 DOM。

声明式路由也遵循这一原则。你不是通过一系列的命令来告诉路由“现在导航到这里,然后加载这个数据”,而是通过清晰、简洁的配置或组件来“声明”你的应用应该有哪些路由、每个路由应该渲染哪个组件、需要加载什么数据。

声明式路由的优势:
* 可读性与可维护性: 路由配置更像是一个应用的结构蓝图,一目了然。
* 可预测性: 状态和行为更易于理解和预测。
* 可测试性: 由于关注点分离,更容易对路由逻辑进行独立测试。
* 更少的错误: 减少了手动操作,降低了人为引入错误的可能性。

1.2 TanStack Router 的核心优势概览

TanStack Router 完美地诠释了声明式路由的精髓,并在此基础上提供了众多开箱即用的高级特性:

1.2.1 端到端类型安全 (End-to-End Type Safety)

这是 TanStack Router 最引人注目的特性之一。得益于其智能的路由配置和自动生成的类型定义,你可以获得:
* 路由路径的类型安全: 编译器会检查你使用的路由路径是否真实存在。
* 动态参数的类型安全: $postId 等动态参数会自动推断出正确的类型。
* 搜索参数的类型安全: 可以为 URL 的搜索参数定义 Zod 模式,确保数据在运行时和编译时都经过验证。
* Loader 数据的类型安全:loader 函数返回的数据可以直接被组件消费,并具有完整的类型提示。
* 导航对象的类型安全: useNavigate 等钩子在跳转时会提供参数的类型校验。

这一切都极大地减少了运行时错误,提升了开发效率和代码质量。

1.2.2 内置数据加载与缓存 (Built-in Data Loading & Caching)

告别组件内数据获取的瀑布效应!TanStack Router 允许你将数据加载逻辑与路由定义 colocated(共置),在路由激活之前就并行获取所需数据。
* Loader 函数: 在每个路由配置中定义 loader 函数,路由激活前自动运行。
* 并行数据获取: 多个路由的 loader 可以并行执行,避免了串行请求的等待。
* 避免瀑布效应: 数据在组件渲染前准备就绪,确保 UI 能够立即展示完整数据。
* 内置缓存: 智能缓存机制,避免重复请求相同数据。
* 预加载 (Prefetching): 用户鼠标悬停在链接上时,自动预加载链接指向路由的数据。

1.2.3 优化的性能与用户体验 (Optimized Performance & User Experience)

除了数据加载优化,TanStack Router 还提供了:
* 懒加载 (Lazy Loading): 轻松实现路由级别的代码分割,按需加载组件,减小初始包体积。
* 智能导航: 平滑的过渡,可以处理网络延迟和加载状态。
* 竞态条件处理: 自动取消过时的请求,避免在快速导航时显示错误数据。

1.2.4 灵活的嵌套路由 (Flexible Nested Routes)

无论是简单的线性路由还是复杂的嵌套布局,TanStack Router 都能轻松应对。你可以定义任意深度的嵌套路由,共享布局组件,并让子路由继承父路由的数据。

1.2.5 强大的开发工具 (Powerful Developer Tools)

提供专门的开发工具,让你能够直观地检查路由状态、历史记录、加载数据和缓存,极大地简化了调试过程。

1.2.6 与 React 生态的完美融合 (Seamless React Integration)

作为为 React 构建的路由库,它充分利用了 React 的 Hooks 范式和组件化思想,让路由逻辑与 React 组件无缝集成。

第二章:准备工作:搭建你的第一个 TanStack Router 应用

在深入代码之前,我们需要做好一些准备工作。

2.1 先决条件

  • Node.js (LTS 版本): 确保你的开发环境安装了 Node.js。
  • 包管理器: npm 或 yarn。
  • React 基础知识: 了解 React 组件、Hooks、JSX 等基本概念。
  • TypeScript (推荐): 虽然 TanStack Router 支持 JavaScript,但为了充分利用其类型安全优势,强烈建议使用 TypeScript。

2.2 项目初始化

我们将使用 Vite 来快速创建一个 React + TypeScript 项目。Vite 是一个现代化的前端构建工具,以其极快的开发服务器启动速度和即时热模块更新而闻名。

  1. 创建 Vite 项目:
    打开你的终端,运行以下命令:
    bash
    npm create vite@latest my-tanstack-router-app -- --template react-ts
    # 或者 yarn create vite my-tanstack-router-app --template react-ts
    # 或者 pnpm create vite my-tanstack-router-app --template react-ts

    这会创建一个名为 my-tanstack-router-app 的新目录,并配置一个基于 React 和 TypeScript 的 Vite 项目。

  2. 进入项目目录并安装依赖:
    bash
    cd my-tanstack-router-app
    npm install # 或者 yarn install / pnpm install

  3. 安装 TanStack Router 依赖:
    现在,安装 TanStack Router 及其 React 适配器,以及开发工具。
    bash
    npm install @tanstack/react-router @tanstack/router-devtools
    # 或者 yarn add @tanstack/react-router @tanstack/router-devtools
    # 或者 pnpm add @tanstack/react-router @tanstack/router-devtools

    如果你的 TypeScript 版本较旧,可能还需要安装 @tanstack/router-cli 作为开发依赖,但对于现代 TypeScript (4.9+) 通常不是必须的,因为类型生成现在更加集成。

  4. 配置 tsconfig.json (重要!):
    为了让 TanStack Router 的类型生成机制正常工作,你需要在 tsconfig.json 中确保 compilerOptions.lib 包含 dom,并且 compilerOptions.stricttrue。通常,Vite 默认配置已经包含了这些,但最好检查一下。

    另外,确保 compilerOptions.baseUrlcompilerOptions.paths 如果有的话,与你的路由文件结构相符。
    最关键的是,需要确保 routeTree.gen.ts 文件被包含在你的项目中,通常它会被 include 选项自动包含。

    “`json
    // tsconfig.json (示例,你的可能略有不同)
    {
    “compilerOptions”: {
    “target”: “ES2020”,
    “useDefineForClassFields”: true,
    “lib”: [“ES2020”, “DOM”, “DOM.Iterable”], // 确保包含 DOM 和 DOM.Iterable
    “module”: “ESNext”,
    “skipLibCheck”: true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    
    /* Linting */
    "strict": true, // 强烈建议设为 true
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    
    // TanStack Router 需要以下配置来正确生成类型
    "plugins": [
      {
        "name": "@tanstack/router-plugin"
      }
    ]
    

    },
    “include”: [“src”, “routeTree.gen.ts”], // 确保包含生成的类型文件
    “references”: [{ “path”: “./tsconfig.node.json” }]
    }
    ``
    **注意:** 如果你的项目没有自动安装
    @tanstack/router-plugin,你需要手动安装它:npm install -D @tanstack/router-plugin`。这个插件是 TanStack Router 类型生成的核心。

现在,你的项目已经准备就绪,可以开始编写路由代码了!

第三章:路由基础:构建你的第一个路由系统

TanStack Router 采用基于文件的路由约定(file-based routing),这意味着你的文件结构将直接定义你的路由。

3.1 核心概念讲解

在开始编码前,我们先了解 TanStack Router 的几个核心构建块:

  • createRouter: 这是创建路由器实例的主要函数。它接收一个根路由(root route)作为参数。
  • createRootRoute: 定义你的应用的根路由。它通常包含一个共享的布局(比如导航栏、页脚)和 <Outlet /> 组件,用于渲染当前子路由的内容。
  • createRoute: 定义一个具体的路由。它关联一个路径、一个父路由(可选)、一个组件,还可以包含数据加载器(loader)、路由守卫(beforeLoad)等。
  • Router 组件: 在你的 React 应用中渲染路由器实例。
  • Link 组件: 用于在应用内部进行导航,类似于 HTML 的 <a> 标签,但不会导致页面刷新。
  • Outlet 组件: 渲染当前匹配的子路由的组件。在嵌套路由中非常重要。
  • routeTree.gen.ts: 这是由 TanStack Router 自动生成的类型定义文件。它根据你的文件结构和 createRoute 调用来生成路由的类型信息,你不应该手动修改这个文件。

3.2 手把手代码示例:你的第一个路由

我们将创建一个简单的应用,包含主页 (/) 和关于页 (/about)。

  1. 创建路由文件目录:
    src 目录下创建一个 routes 文件夹,所有路由相关的组件和逻辑都将放在这里。

    src/
    ├── main.tsx
    ├── App.tsx
    └── routes/
    ├── __root.tsx // 根路由及布局
    ├── index.tsx // 主页
    └── about.tsx // 关于页

  2. src/routes/__root.tsx – 根路由配置

    “`tsx
    // src/routes/__root.tsx
    import { createRootRoute, Outlet } from ‘@tanstack/react-router’
    import { TanStackRouterDevtools } from ‘@tanstack/router-devtools’
    import React from ‘react’

    export const Route = createRootRoute({
    component: () => (
    <>


    Home
    {‘ ‘}

    About

    {/ 这是渲染子路由内容的地方 /}
    {/ 路由开发工具 /}

    ),
    })
    ``
    *
    createRootRoute创建根路由。
    *
    component属性定义了根路由的渲染内容。这里我们添加了一个简单的导航链接和
    *
    是关键,它告诉 TanStack Router 在哪里渲染当前匹配的子路由的组件。
    *
    TanStackRouterDevtools` 是开发工具,方便调试。

  3. src/routes/index.tsx – 主页

    “`tsx
    // src/routes/index.tsx
    import { createFileRoute } from ‘@tanstack/react-router’
    import React from ‘react’

    export const Route = createFileRoute(‘/’)({
    component: () => (

    Welcome Home!

    This is the main page of our application.

    ),
    })
    ``
    *
    createFileRoute(‘/’)createRoute的一个便捷函数,用于基于文件系统自动推断路径。这里的/表示它对应应用的根路径。
    *
    component` 属性定义了主页要渲染的内容。

  4. src/routes/about.tsx – 关于页

    “`tsx
    // src/routes/about.tsx
    import { createFileRoute } from ‘@tanstack/react-router’
    import React from ‘react’

    export const Route = createFileRoute(‘/about’)({
    component: () => (

    About Us

    Learn more about our mission and vision.

    ),
    })
    ``
    *
    createFileRoute(‘/about’)定义了/about` 路径的路由。

  5. src/routeTree.gen.ts – 自动生成的路由树

    这个文件是自动生成的,你不需要手动创建或修改它。在保存上述路由文件后,Vite 的热更新机制(或通过 npm run dev 启动时)会触发 TanStack Router 的插件自动生成这个文件。它包含了所有路由的类型定义和结构。

    “`typescript
    // src/routeTree.gen.ts (自动生成,请勿手动修改)
    / prettier-ignore-start /

    / eslint-disable /
    // @ts-nocheck

    import { FileRoutesByPath } from ‘@tanstack/react-router’

    // Import Routes

    import { Route as RootRoute } from ‘./routes/__root’
    import { Route as IndexRoute } from ‘./routes/index’
    import { Route as AboutRoute } from ‘./routes/about’

    // Create FileRoutesByPath

    type FileRoutesByPathType = FileRoutesByPath<[
    // … 更多路由路径和其对应的路由对象
    ‘/’,
    typeof IndexRoute,
    ‘/about’,
    typeof AboutRoute
    ]>

    declare module ‘@tanstack/react-router’ {
    interface FileRoutesByPath extends FileRoutesByPathType {}
    }

    / prettier-ignore-end /
    “`
    这个文件告诉 TypeScript 你的应用有哪些路由,以及它们的父子关系和参数类型,从而实现端到端类型安全。

  6. src/main.tsx – 路由器实例和渲染

    现在,我们将所有的路由配置组合起来,创建路由器实例,并在 main.tsx 中渲染到 DOM。

    “`tsx
    // src/main.tsx
    import React from ‘react’
    import ReactDOM from ‘react-dom/client’
    import { RouterProvider, createRouter } from ‘@tanstack/react-router’
    import { routeTree } from ‘./routeTree.gen’ // 导入自动生成的路由树

    // 创建路由器实例
    const router = createRouter({ routeTree })

    // 为 TypeScript 提供路由类型(全局声明)
    declare module ‘@tanstack/react-router’ {
    interface Register {
    router: typeof router
    }
    }

    // 渲染你的应用
    const rootElement = document.getElementById(‘root’)!
    if (!rootElement.innerHTML) {
    const root = ReactDOM.createRoot(rootElement)
    root.render(


    ,
    )
    }
    ``
    * 我们从
    routeTree.gen.ts导入了routeTree对象,它包含了所有路由的配置。
    *
    createRouter使用这个路由树创建了一个路由器实例。
    *
    RouterProvider组件负责将路由器实例提供给整个 React 应用。
    *
    declare module ‘@tanstack/react-router’` 这段代码是为 TypeScript 注册你的路由器实例,这样 TanStack Router 就能推断出所有路由的类型信息,从而在你的应用程序中提供无与伦比的类型安全。

  7. 运行你的应用
    在终端中运行:
    bash
    npm run dev

    访问通常是 http://localhost:5173 的地址。你现在应该能看到一个带有“Home”和“About”链接的页面。点击链接,内容会在 <Outlet /> 的位置切换,而页面不会刷新。底部的 TanStack Router DevTools 也会显示路由状态。

3.3 使用 <Link> 组件进行导航

在上面的例子中,我们使用了普通的 <a> 标签。虽然它们能工作,但会触发浏览器重新加载整个页面。为了实现单页应用 (SPA) 的无刷新导航,我们应该使用 TanStack Router 提供的 <Link> 组件。

修改 src/routes/__root.tsx:

“`tsx
// src/routes/__root.tsx
import { createRootRoute, Outlet, Link } from ‘@tanstack/react-router’ // 导入 Link
import { TanStackRouterDevtools } from ‘@tanstack/router-devtools’
import React from ‘react’

export const Route = createRootRoute({
component: () => (
<>

{/ 使用 Link 组件 /}

Home
{‘ ‘}

About




),
})
``
现在,再次运行应用并点击链接,你会发现导航是即时的,没有页面刷新。
组件还会在当前路由匹配时自动添加active` 类名,方便你进行样式定制。

恭喜你!你已经成功搭建了第一个 TanStack Router 应用,并掌握了其基本用法。接下来,我们将深入探索更高级的路由特性。

第四章:深度探索:高级路由特性

4.1 嵌套路由与布局 (Nested Routes & Layouts)

嵌套路由是构建复杂应用的关键。它允许你共享布局、层叠组件,并让子路由继承父路由的数据。

假设我们有一个“帖子”部分,包含一个帖子列表页 (/posts) 和一个单独的帖子详情页 (/posts/$postId)。

  1. 创建 src/routes/posts 目录
    src/
    └── routes/
    ├── __root.tsx
    ├── index.tsx
    ├── about.tsx
    └── posts/
    ├── _layout.tsx // 帖子部分的布局路由
    ├── index.tsx // 帖子列表页
    └── $postId.tsx // 单个帖子详情页 (动态参数)

    • _layout.tsx:这是一个特殊的命名约定。_ 开头的路由文件,表示它是一个父路由,它的子路由会作为 <Outlet /> 渲染在其内部。它本身不会直接匹配 URL,但其路径将作为前缀。
    • $postId.tsx$ 开头表示这是一个动态路由参数。
  2. src/routes/posts/_layout.tsx – 帖子部分共享布局

    “`tsx
    // src/routes/posts/_layout.tsx
    import { createFileRoute, Outlet, Link } from ‘@tanstack/react-router’
    import React from ‘react’

    // 定义帖子部分根路由,路径为 /posts
    export const Route = createFileRoute(‘/posts’)({
    component: () => (

    Posts Section


    {/ 渲染子路由内容 /}

    ),
    })
    ``
    *
    createFileRoute(‘/posts’)定义了/posts路径的路由。
    * 此布局路由会渲染
    ,其子路由(posts/index.tsxposts/$postId.tsx`)将在这里显示。

  3. src/routes/posts/index.tsx – 帖子列表页

    “`tsx
    // src/routes/posts/index.tsx
    import { createFileRoute, Link } from ‘@tanstack/react-router’
    import React from ‘react’

    // 假设有一些帖子数据
    const posts = [
    { id: 1, title: ‘First Post’, content: ‘This is the first post.’ },
    { id: 2, title: ‘Second Post’, content: ‘This is the second post.’ },
    { id: 3, title: ‘Third Post’, content: ‘This is the third post.’ },
    ]

    export const Route = createFileRoute(‘/posts/’)({ // 注意这里的路径 ‘/posts/’
    component: () => (

    Posts List

      {posts.map((post) => (

    • /posts/${post.id}} className=”hover:underline”>
      {post.title}
    • ))}

    ),
    })
    ``
    *
    createFileRoute(‘/posts/’):这里的/posts/表示这个路由匹配/posts这个路径,并且是posts/_layout.tsx` 的子路由。

  4. src/routes/posts/$postId.tsx – 单个帖子详情页 (动态参数)

    “`tsx
    // src/routes/posts/$postId.tsx
    import { createFileRoute } from ‘@tanstack/react-router’
    import React from ‘react’

    // 假设这是我们的数据源,通常会从 API 获取
    const postsData = [
    { id: 1, title: ‘First Post’, content: ‘This is the first post.’ },
    { id: 2, title: ‘Second Post’, content: ‘This is the second post.’ },
    { id: 3, title: ‘Third Post’, content: ‘This is the third post.’ },
    ]

    export const Route = createFileRoute(‘/posts/$postId’)({
    component: () => {
    const { postId } = Route.useParams() // 获取动态路由参数
    const post = postsData.find((p) => p.id === parseInt(postId))

    if (!post) {
      return <div className="p-2">Post not found!</div>
    }
    
    return (
      <div className="p-2">
        <h3>{post.title}</h3>
        <p>{post.content}</p>
        <Link to="/posts" className="text-blue-500 hover:underline">
          Back to Posts
        </Link>
      </div>
    )
    

    },
    })
    ``
    *
    createFileRoute(‘/posts/$postId’)$postId表示这是一个动态路由参数,它会匹配/posts/123这样的 URL。
    *
    Route.useParams()钩子用于在组件中获取当前路由的所有动态参数。
    * TanStack Router 会自动为
    postId` 提供类型安全,基于路由定义。

  5. 更新根布局导航
    别忘了在 src/routes/__root.tsx 中添加指向 /posts 的链接:

    tsx
    // src/routes/__root.tsx
    // ... (imports)
    export const Route = createRootRoute({
    component: () => (
    <>
    <div className="p-2 flex gap-2">
    <Link to="/" className="[&.active]:font-bold">Home</Link>{' '}
    <Link to="/about" className="[&.active]:font-bold">About</Link>{' '}
    <Link to="/posts" className="[&.active]:font-bold">Posts</Link> {/* 添加 Posts 链接 */}
    </div>
    <hr />
    <Outlet />
    <TanStackRouterDevtools position="bottom-right" />

    ),
    })

现在,刷新浏览器,点击“Posts”链接,你将看到帖子列表。点击任意帖子标题,会导航到详情页,并且 URL 会相应变化,但顶部导航栏和“Posts Section”标题会保持不变,因为它们属于父布局。

4.2 搜索参数 (Search Parameters)

除了动态路由参数,URL 中的查询字符串(例如 ?page=2&category=tech)也是常见的参数类型。TanStack Router 提供了强大的搜索参数处理机制,包括类型安全和验证。

假设我们希望在帖子列表页支持分页和筛选。

  1. 修改 src/routes/posts/index.tsx

    我们将为 /posts 路由定义一个搜索参数模式,并使用 useSearch() 钩子来访问它们。

    “`tsx
    // src/routes/posts/index.tsx
    import { createFileRoute, Link, useNavigate } from ‘@tanstack/react-router’
    import React from ‘react’
    import { z } from ‘zod’ // 引入 Zod 进行参数验证

    // 假设有一些帖子数据
    const allPosts = [
    { id: 1, title: ‘First Post’, content: ‘This is the first post.’, category: ‘tech’ },
    { id: 2, title: ‘Second Post’, content: ‘This is the second post.’, category: ‘lifestyle’ },
    { id: 3, title: ‘Third Post’, content: ‘This is the third post.’, category: ‘tech’ },
    { id: 4, title: ‘Fourth Post’, content: ‘Another one.’, category: ‘lifestyle’ },
    { id: 5, title: ‘Fifth Post’, content: ‘Final post.’, category: ‘tech’ },
    ]

    // 定义搜索参数的 Zod 模式,确保类型安全和运行时验证
    const postsSearchSchema = z.object({
    page: z.number().catch(1), // 默认页码 1
    category: z.enum([‘tech’, ‘lifestyle’]).catch(‘all’).optional(), // 类别可选
    })

    export const Route = createFileRoute(‘/posts/’)({
    component: PostsIndex,
    validateSearch: postsSearchSchema, // 应用搜索参数验证
    })

    function PostsIndex() {
    // 使用 useSearch 获取类型安全的搜索参数
    const { page, category } = Route.useSearch()
    const navigate = useNavigate({ from: Route.fullPath }) // 用于程序化导航

    const filteredPosts = category && category !== ‘all’
    ? allPosts.filter(post => post.category === category)
    : allPosts;

    // 简单的分页逻辑
    const pageSize = 2;
    const paginatedPosts = filteredPosts.slice((page – 1) * pageSize, page * pageSize);
    const totalPages = Math.ceil(filteredPosts.length / pageSize);

    const handlePageChange = (newPage: number) => {
    navigate({
    search: (prev) => ({ …prev, page: newPage }),
    })
    }

    const handleCategoryChange = (newCategory: ‘all’ | ‘tech’ | ‘lifestyle’) => {
    navigate({
    search: (prev) => ({ …prev, category: newCategory, page: 1 }), // 切换类别时重置页码
    })
    }

    return (

    Posts List (Page: {page}, Category: {category || ‘All’})



      {paginatedPosts.length > 0 ? (
      paginatedPosts.map((post) => (

    • /posts/${post.id}} className=”hover:underline”>
      {post.title} (Category: {post.category})
    • ))
      ) : (

    • No posts found for this category.
    • )}


    Page {page} of {totalPages}

    )
    }
    ``
    * **安装 Zod:**
    npm install zodyarn add zod
    *
    validateSearch: postsSearchSchema:这是关键。它将 Zod 模式与路由关联起来,确保 URL 中的search参数在进入组件之前被验证和解析。如果参数不符合模式,它将使用catch定义的默认值。
    *
    Route.useSearch():返回类型安全的搜索参数对象,例如{ page: number, category?: ‘tech’ | ‘lifestyle’ }
    *
    useNavigate():用于程序化导航,可以通过search` 属性更新 URL 的查询字符串。

现在,访问 /posts,然后尝试点击分类按钮或分页按钮,你会看到 URL 发生变化,并且数据会相应地过滤和分页。同时,如果你手动在 URL 中输入 ?page=abc,TanStack Router 会自动将其转换为 ?page=1,因为我们用 z.number().catch(1) 定义了默认值。

4.3 路由守卫与重定向 (Route Guards & Redirects)

路由守卫(或称为导航守卫)允许你在用户访问某个路由之前或之后执行逻辑,常用于身份验证、授权或数据预加载检查。TanStack Router 提供了 beforeLoad 钩子来实现这一点。

假设我们有一个需要认证才能访问的 /dashboard 路由。

  1. 创建模拟认证服务
    创建一个 src/auth.ts 文件来模拟用户认证状态。

    “`typescript
    // src/auth.ts
    // 简单的模拟认证状态
    let isAuthenticated = false;

    export const login = () => {
    isAuthenticated = true;
    console.log(‘User logged in.’);
    };

    export const logout = () => {
    isAuthenticated = false;
    console.log(‘User logged out.’);
    };

    export const checkAuth = () => isAuthenticated;
    “`

  2. 创建 /dashboard 路由
    “`tsx
    // src/routes/dashboard.tsx
    import { createFileRoute, redirect } from ‘@tanstack/react-router’
    import React from ‘react’
    import { checkAuth, login, logout } from ‘../auth’

    export const Route = createFileRoute(‘/dashboard’)({
    beforeLoad: ({ location }) => {
    // 在路由加载前检查认证状态
    if (!checkAuth()) {
    console.log(‘Not authenticated, redirecting to home.’);
    // 如果未认证,重定向到主页,并带上回跳参数
    throw redirect({
    to: ‘/’,
    search: {
    redirect: location.href,
    },
    })
    }
    },
    component: () => (

    Welcome to the Dashboard!

    This content is only visible to authenticated users.

    ),
    })
    ``
    *
    beforeLoad:这是一个异步函数,在路由的loader运行之前执行。
    *
    checkAuth():我们模拟的认证函数。
    *
    redirect({ to: ‘/’, search: { redirect: location.href } }):如果用户未认证,我们通过throw redirect()来强制进行重定向。location.href作为redirect` 搜索参数传递,可以在登录页处理完成后跳回原页面。

  3. 更新根布局和主页以支持登录/登出
    修改 src/routes/__root.tsx 添加仪表盘链接,并更新 src/routes/index.tsx 来模拟登录。

    src/routes/__root.tsx:
    “`tsx
    // src/routes/__root.tsx
    // … (imports)
    import { checkAuth } from ‘../auth’; // 导入认证状态检查

    export const Route = createRootRoute({
    component: () => (
    <>

    Home{‘ ‘}
    About{‘ ‘}
    Posts{‘ ‘}
    {/ 只有在登录状态才显示 Dashboard 链接 /}
    {checkAuth() && (
    Dashboard
    )}




    ),
    })
    “`

    src/routes/index.tsx:
    “`tsx
    // src/routes/index.tsx
    import { createFileRoute, useNavigate } from ‘@tanstack/react-router’
    import React from ‘react’
    import { login, checkAuth } from ‘../auth’;
    import { z } from ‘zod’; // 引入 Zod 进行参数验证

    // 定义搜索参数的 Zod 模式,用于处理重定向
    const indexSearchSchema = z.object({
    redirect: z.string().optional(),
    });

    export const Route = createFileRoute(‘/’)({
    component: IndexPage,
    validateSearch: indexSearchSchema, // 应用搜索参数验证
    })

    function IndexPage() {
    const navigate = useNavigate();
    const { redirect: redirectTo } = Route.useSearch(); // 获取重定向参数
    const authenticated = checkAuth();

    const handleLogin = () => {
    login();
    // 如果有重定向地址,登录后跳转过去
    if (redirectTo) {
    navigate({ to: redirectTo });
    } else {
    // 否则刷新当前路由,触发根路由的重新渲染,显示Dashboard链接
    navigate({ to: ‘/’ });
    }
    };

    return (

    Welcome Home!

    This is the main page of our application.

    {!authenticated ? (

    ) : (

    You are currently logged in.

    )}

    )
    }
    “`

现在,当你直接访问 /dashboard 时,会被重定向到 /。点击主页的“Login”按钮后,你会看到“Dashboard”链接出现,并且可以成功访问仪表盘页面。在仪表盘点击“Logout”,状态会重置。

4.4 数据加载器 (Loaders) 与数据处理 (Data Handling)

这是 TanStack Router 最强大的特性之一,它将数据获取与路由定义紧密结合,解决了传统路由方案中的许多痛点。

  1. Loader 函数的定义与执行时机
    loader 函数是 createRoute 配置对象的一部分。它在路由进入时,且在组件渲染 之前 执行。这意味着当你的组件首次渲染时,所需数据已经可用,避免了加载状态和 UI 闪烁。

    优点:
    * 避免瀑布效应: 多个路由的 loader 可以并行运行,大大减少了数据加载时间。
    * 更好的用户体验: 用户在看到页面内容时,数据已经存在,无需额外的加载指示。
    * 更好的代码组织: 数据加载逻辑与路由定义共置,提高可维护性。
    * 类型安全: loader 返回的数据具有完整的 TypeScript 类型。

  2. posts/$postId.tsx 中使用 Loader
    我们将修改帖子详情页,让它使用 loader 来获取帖子数据,而不是直接在组件内部查找。

    “`tsx
    // src/routes/posts/$postId.tsx
    import { createFileRoute, notFound, Link } from ‘@tanstack/react-router’
    import React from ‘react’

    // 假设这是我们的数据源
    const postsData = [
    { id: 1, title: ‘First Post (via Loader)’, content: ‘This is the first post.’, category: ‘tech’ },
    { id: 2, title: ‘Second Post (via Loader)’, content: ‘This is the second post.’, category: ‘lifestyle’ },
    { id: 3, title: ‘Third Post (via Loader)’, content: ‘This is the third post.’, category: ‘tech’ },
    ]

    export const Route = createFileRoute(‘/posts/$postId’)({
    loader: async ({ params }) => {
    // 在这里进行数据获取,例如发起 API 请求
    console.log(Fetching post with ID: ${params.postId});
    await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟

    const post = postsData.find((p) => p.id === parseInt(params.postId));
    
    if (!post) {
      throw notFound({ code: 'POST_NOT_FOUND', message: `Post with ID ${params.postId} not found.` });
    }
    return { post }; // loader 必须返回一个对象
    

    },
    component: PostDetail,
    // 可以在这里定义错误组件,来捕获 loader 抛出的错误
    errorComponent: ({ error }) => {
    if (error.status === 404) {
    return

    Post not found!

    ;
    }
    return

    An error occurred: {error.message}

    ;
    }
    })

    function PostDetail() {
    // 使用 useLoaderData 获取 loader 返回的数据,它具有类型安全
    const { post } = Route.useLoaderData()

    return (

    {post.title}

    {post.content}


    Back to Posts

    )
    }
    ``
    *
    loader: async ({ params }) => { … }loader函数是一个异步函数,它接收一个包含路由参数 (params)、搜索参数 (search)、父路由数据 (parentData) 等上下文信息的对象。
    *
    params.postId:类型安全地访问动态路由参数。
    *
    throw notFound(…):如果数据未找到,可以抛出notFound错误,路由系统会自动处理 404 状态。
    *
    return { post }loader必须返回一个对象,其属性可以在组件中通过useLoaderData()访问。
    *
    Route.useLoaderData():在组件内部,使用这个钩子来获取loader` 返回的数据,它完全是类型安全的。

  3. 处理加载状态 (Pending States)
    loader 正在加载数据时,页面会处于“待定”(pending)状态。你可以使用 useIsFetching()pendingComponent 来显示加载指示器。

    posts/_layout.tsx 中添加一个全局的加载指示器:
    “`tsx
    // src/routes/posts/_layout.tsx
    import { createFileRoute, Outlet, Link, useIsFetching } from ‘@tanstack/react-router’ // 导入 useIsFetching
    import React from ‘react’

    export const Route = createFileRoute(‘/posts’)({
    component: () => {
    const isFetching = useIsFetching(); // 检查是否有 loader 正在加载数据
    return (

    Posts Section

    {isFetching > 0 &&

    Loading posts…

    } {/ 加载指示器 /}


    )
    },
    })
    “`
    现在,当你点击帖子链接时,会有一个短暂的“Loading posts…”提示。

  4. 错误处理 (Error Handling)
    如果 loader 抛出错误(除了 notFound 之外的错误),你可以使用路由的 errorComponent 属性来渲染一个特定的错误 UI。

    posts/$postId.tsx 中已经添加了 errorComponent 示例,它可以捕获 loader 抛出的任何错误,并根据错误类型(如 404)显示不同的信息。

4.5 路由上下文 (Route Context)

TanStack Router 允许你为路由定义一个上下文,这个上下文可以在所有子路由中访问。这对于传递全局信息或父级数据非常有用。

例如,我们可以在根路由中定义一个用户上下文。

  1. 修改 src/routes/__root.tsx
    “`tsx
    // src/routes/__root.tsx
    import { createRootRoute, Outlet, Link, createRouterContext } from ‘@tanstack/react-router’
    import { TanStackRouterDevtools } from ‘@tanstack/router-devtools’
    import React from ‘react’
    import { checkAuth } from ‘../auth’;

    // 定义路由上下文的类型
    interface MyRouterContext {
    isAuthenticated: boolean;
    currentUser: { id: string; name: string } | null;
    }

    // 创建路由上下文
    export const RouterContext = createRouterContext();

    export const Route = createRootRoute({
    context: {
    // 在这里提供上下文值
    isAuthenticated: checkAuth(),
    currentUser: checkAuth() ? { id: ‘user-123’, name: ‘John Doe’ } : null,
    },
    component: () => {
    // 使用 RouterContext.use() 访问上下文
    const { isAuthenticated, currentUser } = RouterContext.use();

    return (
      <>
        <div className="p-2 flex gap-2">
          <Link to="/" className="[&.active]:font-bold">Home</Link>{' '}
          <Link to="/about" className="[&.active]:font-bold">About</Link>{' '}
          <Link to="/posts" className="[&.active]:font-bold">Posts</Link>{' '}
          {isAuthenticated && (
            <Link to="/dashboard" className="[&.active]:font-bold">Dashboard</Link>
          )}
          {currentUser && <span className="ml-auto">Welcome, {currentUser.name}!</span>}
        </div>
        <hr />
        <Outlet />
        <TanStackRouterDevtools position="bottom-right" />
      
    )
    

    },
    })

    ``
    *
    createRouterContext():创建带有指定类型的路由上下文。
    *
    context属性:在createRootRoute中定义上下文的初始值。
    *
    RouterContext.use()`:在任何子路由组件或其子组件中,都可以通过这个钩子访问上下文数据。

  2. 在子路由中使用上下文
    现在,你可以在 /dashboard.tsx 或其他任何子路由中访问 isAuthenticatedcurrentUser

    “`tsx
    // src/routes/dashboard.tsx (部分修改)
    import { createFileRoute, redirect } from ‘@tanstack/react-router’
    import React from ‘react’
    import { checkAuth, login, logout } from ‘../auth’
    import { Route as RootRoute } from ‘./__root’; // 导入根路由以访问其上下文

    export const Route = createFileRoute(‘/dashboard’)({
    // 在这里可以访问父路由(根路由)的上下文
    // 例如,可以在 beforeLoad 中直接使用 rootRoute.use().isAuthenticated
    beforeLoad: ({ location, context: { isAuthenticated } }) => { // 通过 context 参数直接解构
    if (!isAuthenticated) {
    throw redirect({
    to: ‘/’,
    search: {
    redirect: location.href,
    },
    })
    }
    },
    component: () => {
    const { currentUser } = RootRoute.use(); // 从根路由上下文获取 currentUser
    return (

    Welcome to the Dashboard, {currentUser?.name || ‘Guest’}!

    This content is only visible to authenticated users.

    )
    },
    })
    ``
    通过
    context参数或RootRoute.use()`,你可以在子路由中访问父路由提供的上下文,这使得跨组件和路由共享数据变得非常高效和类型安全。

第五章:性能优化与开发体验

TanStack Router 不仅功能强大,还在性能和开发体验方面做了诸多优化。

5.1 懒加载与代码分割 (Lazy Loading & Code Splitting)

TanStack Router 天生支持路由级别的代码分割。由于它采用基于文件的路由约定,每个路由文件就是一个独立的模块。当应用程序打包时(如使用 Vite 或 Webpack),这些文件可以被分割成独立的 chunk。只有当用户导航到特定路由时,相应的 chunk 才会被加载。

你不需要做任何特殊配置,只需在 createFileRoute 中导入你的组件,构建工具会负责处理:
“`typescript
// src/routes/dashboard.tsx
// …
export const Route = createFileRoute(‘/dashboard’)({
// …
component: DashboardComponent, // 这里的 DashboardComponent 将被懒加载
})

// 或直接定义为匿名组件
export const Route = createFileRoute(‘/dashboard’)({
component: () =>

Dashboard Content

,
})
“`
这极大地减少了应用的初始加载时间,提升了首次内容绘制 (FCP) 和用户体验。

5.2 预加载 (Prefetching)

TanStack Router 具有智能的预加载机制:
* 悬停预加载: 当用户鼠标悬停在 <Link> 组件上时,TanStack Router 会自动预加载该链接指向路由的所有 loader 数据,甚至可以预加载路由组件本身。这使得用户点击链接后的导航几乎是瞬时的。
* 手动预加载: 虽然自动预加载已经足够智能,但你也可以通过 router.preloadRoute('/some-path') 等 API 进行手动预加载。

5.3 错误边界 (Error Boundaries)

除了前面提到的 errorComponent 可以在路由级别处理 loader 抛出的错误外,你也可以在路由树的更上层定义错误边界。
* createRootRouteerrorComponent 用于捕获所有子路由未处理的错误。
* 父路由的 errorComponent 可以捕获其自身 loader 及其子路由 loader 的错误。
* 你也可以在 React 组件内部使用 React.ErrorBoundary 来创建更细粒度的错误边界。

这种分层的错误处理机制确保了即使某个路由或其数据加载失败,整个应用也不会崩溃,而是优雅地显示错误信息。

5.4 开发工具 (DevTools)

我们之前已经在 __root.tsx 中集成了 TanStackRouterDevtools。这些开发工具非常有用:
* 检查路由状态: 查看当前 URL、匹配的路由、动态参数、搜索参数。
* 查看 Loader 数据: 检查每个路由加载的数据,以及其缓存状态。
* 历史记录: 浏览导航历史。
* 性能洞察: 查看 loader 的执行时间。

这些工具是调试和理解路由行为的强大助手。

5.5 TypeScript 集成

这是 TanStack Router 的核心卖点。通过自动生成的 routeTree.gen.ts 文件和巧妙的类型推断,你几乎可以在所有路由相关的操作中获得完整的类型安全:
* to 属性的路径校验: <Link to="/non-existent-path" /> 会报错。
* 动态参数的类型: Route.useParams().postId 会自动是 stringnumber (如果你用 Zod 转换过)。
* 搜索参数的类型: Route.useSearch().page 会是 number
* Loader 数据的类型: Route.useLoaderData().post 会是 { id: number; title: string; content: string; }
* Context 的类型: RouterContext.use().currentUser 会是 { id: string; name: string } | null

这一切都意味着你可以在开发阶段捕获大量错误,而不是等到运行时才发现。

第六章:常见问题与最佳实践

6.1 如何处理 404 页面?

当用户访问一个不存在的 URL 时,TanStack Router 会自动处理 404 状态。你可以在 createRootRoute 中定义 notFoundComponent 来显示一个全局的 404 页面:

“`tsx
// src/routes/__root.tsx
import { createRootRoute, Outlet, Link } from ‘@tanstack/react-router’
// …

export const Route = createRootRoute({
component: () => (
// … 正常布局

// …
),
notFoundComponent: () => (

404 – Page Not Found

Sorry, the page you are looking for does not exist.

Go Home

),
})
``
如果
loader抛出notFound()错误,也会触发这个notFoundComponent`。

6.2 如何进行认证与授权?

我们在 4.3 路由守卫与重定向 中已经详细介绍了如何使用 beforeLoad 钩子进行认证检查和重定向。对于更复杂的授权(例如,只有管理员才能访问某个页面),你可以在 beforeLoad 中结合用户角色信息进行判断:

tsx
// 示例:仅管理员可访问
export const Route = createFileRoute('/admin')({
beforeLoad: async ({ context: { currentUser } }) => {
if (!currentUser || currentUser.role !== 'admin') {
throw redirect({ to: '/unauthorized' }); // 或重定向到登录页
}
},
// ...
})

结合路由上下文 (context) 和 beforeLoad,你可以构建出非常灵活和强大的认证授权体系。

6.3 如何进行动画过渡?

TanStack Router 本身不提供动画过渡功能,但它提供了 isPending 状态,你可以利用它结合 CSS 或第三方动画库(如 Framer Motion, React Spring)来实现过渡效果。

例如,你可以观察 useTransition()useIsFetching() 的状态,然后在状态变化时应用 CSS 类或触发动画:

“`tsx
// 示例:在根路由中显示全局加载动画
import { createRootRoute, Outlet, useIsFetching, useTransition } from ‘@tanstack/react-router’
// …
export const Route = createRootRoute({
component: () => {
const isFetching = useIsFetching(); // 任何 loader 正在加载
const transition = useTransition(); // 路由切换进行中

return (
  <>
    {/* 全局加载条 */}
    {(isFetching > 0 || transition.state === 'pending') && (
      <div className="fixed top-0 left-0 w-full h-1 bg-blue-500 animate-pulse z-50"></div>
    )}
    <div className="p-2 flex gap-2">
      {/* ... */}
    </div>
    <hr />
    {/* 路由内容过渡动画 */}
    <div
      className={`
        transition-opacity duration-300 ease-in-out
        ${transition.state === 'pending' ? 'opacity-0' : 'opacity-100'}
      `}
    >
      <Outlet />
    </div>
    {/* ... */}
  
)

},
})
“`
通过观察这些状态,你可以精确地控制路由切换时的视觉反馈。

6.4 如何与状态管理库集成?

TanStack Router 与任何状态管理库(如 Redux, Zustand, Recoil, Jotai, Context API)都能很好地集成。
* 全局状态: 对于全局共享的状态,你可以在你的根组件(App.tsxmain.tsx)中包裹你的 RouterProvider,使其能够访问你状态管理库提供的 provider。
* 路由特定状态: 对于与特定路由相关的数据,强烈建议使用 loader 函数。Loader 函数天然地将数据获取与路由解耦,并提供了更好的性能和类型安全。

6.5 路由参数验证与 Loader 数据验证 (Zod)

TanStack Router 强烈推荐使用 Zod 进行参数验证。它不仅提供了运行时验证,还能在编译时提供优秀的类型推断。

  • 动态路由参数验证:
    你可以在 createFileRoute 中使用 parseParams 属性来验证动态路由参数。
    “`tsx
    import { createFileRoute } from ‘@tanstack/react-router’
    import { z } from ‘zod’

    export const Route = createFileRoute(‘/users/$userId’)({
    // 定义参数的 Zod 模式
    parseParams: (params) => z.object({ userId: z.number().int().positive() }).parse(params),
    loader: ({ params }) => {
    // params.userId 现在保证是 number 类型
    return { user: fetchUserById(params.userId) };
    },
    // …
    })
    ``
    如果
    userId无法解析为正整数,parseParams会抛出错误,触发errorComponent`。

  • 搜索参数验证:
    我们在 4.2 搜索参数 中已经展示了如何使用 validateSearch 来验证搜索参数。

  • Loader 数据验证:
    虽然 loader 返回的数据本身是类型安全的,但如果你的数据来自外部 API,你可能需要对原始响应进行运行时验证。你可以在 loader 函数内部使用 Zod:
    “`tsx
    // src/routes/posts/$postId.tsx (loader 内部)
    const PostSchema = z.object({
    id: z.number(),
    title: z.string(),
    content: z.string(),
    category: z.string(),
    });

    export const Route = createFileRoute(‘/posts/$postId’)({
    loader: async ({ params }) => {
    // 模拟 API 调用
    const response = await fetch(/api/posts/${params.postId});
    if (!response.ok) {
    throw new Error(‘Failed to fetch post’);
    }
    const rawData = await response.json();
    // 验证并解析数据
    const post = PostSchema.parse(rawData);
    return { post };
    },
    // …
    })
    “`
    这样,即使 API 返回了意外格式的数据,你的应用程序也能在运行时捕获并处理。

6.6 组织你的路由文件

随着应用程序的增长,路由文件可能会变得非常多。以下是一些建议:
* 扁平化与嵌套结合: 对于简单的路由,可以直接放在 src/routes/ 下。对于有共同前缀和布局的复杂功能模块,使用嵌套文件夹(如 src/routes/admin/, src/routes/user/)。
* _layout.tsx 利用 _layout.tsx 文件来定义共享布局和父路由的数据加载逻辑。
* index.tsx 模块的根路径通常由 index.tsx 文件表示。
* $param.tsx 动态路由参数使用 $ 前缀的文件名。
* 将组件与路由定义分离: 对于复杂的组件,可以将其定义在路由文件之外,然后导入到路由定义中。
src/
└── routes/
├── posts/
│ ├── _layout.tsx
│ ├── index.tsx
│ └── $postId.tsx
└── components/ // 存放通用的路由组件
├── PostDetailComponent.tsx
└── PostListComponent.tsx

“`tsx
// src/routes/posts/$postId.tsx
import { createFileRoute } from ‘@tanstack/react-router’
import { PostDetailComponent } from ‘../../components/PostDetailComponent’; // 导入组件

export const Route = createFileRoute('/posts/$postId')({
  // ... loader
  component: PostDetailComponent, // 直接使用导入的组件
})
```
这种方式使得路由文件保持简洁,只专注于路由配置,而组件专注于 UI 渲染。

总结:轻松玩转现代声明式路由

通过本文的详细介绍和实践,相信你已经对 TanStack Router 有了全面的了解,并能够轻松地将其应用于你的现代 React 项目中。

我们回顾了 TanStack Router 的核心优势:
* 端到端类型安全,显著减少运行时错误。
* 内置数据加载与缓存,彻底解决数据瀑布效应,提升用户体验。
* 灵活的嵌套路由强大的路由守卫,构建复杂应用逻辑游刃有余。
* 智能预加载、懒加载等性能优化机制,确保应用极致流畅。
* 强大的开发工具无缝的 TypeScript 集成,提供卓越的开发者体验。

TanStack Router 不仅仅是一个路由库,它是一种全新的路由开发范式。它将路由、数据加载、类型安全和性能优化整合到一个内聚的解决方案中,使得构建高性能、高可维护性的复杂单页应用成为可能。

告别传统路由的繁琐配置和潜在风险,拥抱 TanStack Router 带来的声明式、类型安全的现代路由新体验吧!从现在开始,你将能够更加自信、高效地构建出令人惊叹的 Web 应用。

探索和实践是最好的学习方式。现在,就开始你的 TanStack Router 之旅吧!

发表评论

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

滚动至顶部