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 是一个现代化的前端构建工具,以其极快的开发服务器启动速度和即时热模块更新而闻名。
-
创建 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 项目。 -
进入项目目录并安装依赖:
bash
cd my-tanstack-router-app
npm install # 或者 yarn install / pnpm install -
安装 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+) 通常不是必须的,因为类型生成现在更加集成。 -
配置
tsconfig.json(重要!):
为了让 TanStack Router 的类型生成机制正常工作,你需要在tsconfig.json中确保compilerOptions.lib包含dom,并且compilerOptions.strict为true。通常,Vite 默认配置已经包含了这些,但最好检查一下。另外,确保
compilerOptions.baseUrl和compilerOptions.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)。
-
创建路由文件目录:
在src目录下创建一个routes文件夹,所有路由相关的组件和逻辑都将放在这里。src/
├── main.tsx
├── App.tsx
└── routes/
├── __root.tsx // 根路由及布局
├── index.tsx // 主页
└── about.tsx // 关于页 -
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: () => (
<>
{/ 这是渲染子路由内容的地方 /}
{/ 路由开发工具 /}
),
})
``createRootRoute
*创建根路由。component
*属性定义了根路由的渲染内容。这里我们添加了一个简单的导航链接和。
*是关键,它告诉 TanStack Router 在哪里渲染当前匹配的子路由的组件。TanStackRouterDevtools` 是开发工具,方便调试。
* -
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` 属性定义了主页要渲染的内容。
* -
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` 路径的路由。 -
src/routeTree.gen.ts– 自动生成的路由树这个文件是自动生成的,你不需要手动创建或修改它。在保存上述路由文件后,Vite 的热更新机制(或通过
npm run dev启动时)会触发 TanStack Router 的插件自动生成这个文件。它包含了所有路由的类型定义和结构。“`typescript
// src/routeTree.gen.ts (自动生成,请勿手动修改)
/ prettier-ignore-start // eslint-disable /
// @ts-nocheckimport { 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 你的应用有哪些路由,以及它们的父子关系和参数类型,从而实现端到端类型安全。 -
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 就能推断出所有路由的类型信息,从而在你的应用程序中提供无与伦比的类型安全。
* -
运行你的应用
在终端中运行:
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: () => (
<>
Home
{‘ ‘}
About
),
})
``
现在,再次运行应用并点击链接,你会发现导航是即时的,没有页面刷新。组件还会在当前路由匹配时自动添加active` 类名,方便你进行样式定制。
恭喜你!你已经成功搭建了第一个 TanStack Router 应用,并掌握了其基本用法。接下来,我们将深入探索更高级的路由特性。
第四章:深度探索:高级路由特性
4.1 嵌套路由与布局 (Nested Routes & Layouts)
嵌套路由是构建复杂应用的关键。它允许你共享布局、层叠组件,并让子路由继承父路由的数据。
假设我们有一个“帖子”部分,包含一个帖子列表页 (/posts) 和一个单独的帖子详情页 (/posts/$postId)。
-
创建
src/routes/posts目录
src/
└── routes/
├── __root.tsx
├── index.tsx
├── about.tsx
└── posts/
├── _layout.tsx // 帖子部分的布局路由
├── index.tsx // 帖子列表页
└── $postId.tsx // 单个帖子详情页 (动态参数)_layout.tsx:这是一个特殊的命名约定。_开头的路由文件,表示它是一个父路由,它的子路由会作为<Outlet />渲染在其内部。它本身不会直接匹配 URL,但其路径将作为前缀。$postId.tsx:$开头表示这是一个动态路由参数。
-
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.tsx和posts/$postId.tsx`)将在这里显示。 -
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` 的子路由。 -
/posts/${post.id}} className=”hover:underline”>
-
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()
*钩子用于在组件中获取当前路由的所有动态参数。postId` 提供类型安全,基于路由定义。
* TanStack Router 会自动为 -
更新根布局导航
别忘了在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 提供了强大的搜索参数处理机制,包括类型安全和验证。
假设我们希望在帖子列表页支持分页和筛选。
-
修改
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 ? (
-
/posts/${post.id}} className=”hover:underline”>
{post.title} (Category: {post.category})
- No posts found for this category.
paginatedPosts.map((post) => ())
) : ()}
Page {page} of {totalPages}
)
}
``npm install zod
* **安装 Zod:**或yarn add zod。validateSearch: postsSearchSchema
*:这是关键。它将 Zod 模式与路由关联起来,确保 URL 中的search参数在进入组件之前被验证和解析。如果参数不符合模式,它将使用catch定义的默认值。Route.useSearch()
*:返回类型安全的搜索参数对象,例如{ page: number, category?: ‘tech’ | ‘lifestyle’ }。useNavigate()
*:用于程序化导航,可以通过search` 属性更新 URL 的查询字符串。 -
/posts/${post.id}} className=”hover:underline”>
现在,访问 /posts,然后尝试点击分类按钮或分页按钮,你会看到 URL 发生变化,并且数据会相应地过滤和分页。同时,如果你手动在 URL 中输入 ?page=abc,TanStack Router 会自动将其转换为 ?page=1,因为我们用 z.number().catch(1) 定义了默认值。
4.3 路由守卫与重定向 (Route Guards & Redirects)
路由守卫(或称为导航守卫)允许你在用户访问某个路由之前或之后执行逻辑,常用于身份验证、授权或数据预加载检查。TanStack Router 提供了 beforeLoad 钩子来实现这一点。
假设我们有一个需要认证才能访问的 /dashboard 路由。
-
创建模拟认证服务
创建一个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;
“` -
创建
/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` 搜索参数传递,可以在登录页处理完成后跳回原页面。 -
更新根布局和主页以支持登录/登出
修改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 最强大的特性之一,它将数据获取与路由定义紧密结合,解决了传统路由方案中的许多痛点。
-
Loader 函数的定义与执行时机
loader函数是createRoute配置对象的一部分。它在路由进入时,且在组件渲染 之前 执行。这意味着当你的组件首次渲染时,所需数据已经可用,避免了加载状态和 UI 闪烁。优点:
* 避免瀑布效应: 多个路由的loader可以并行运行,大大减少了数据加载时间。
* 更好的用户体验: 用户在看到页面内容时,数据已经存在,无需额外的加载指示。
* 更好的代码组织: 数据加载逻辑与路由定义共置,提高可维护性。
* 类型安全:loader返回的数据具有完整的 TypeScript 类型。 -
在
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) {
returnPost not found!;
}
returnAn 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` 返回的数据,它完全是类型安全的。 -
处理加载状态 (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…”提示。 -
错误处理 (Error Handling)
如果loader抛出错误(除了notFound之外的错误),你可以使用路由的errorComponent属性来渲染一个特定的错误 UI。在
posts/$postId.tsx中已经添加了errorComponent示例,它可以捕获loader抛出的任何错误,并根据错误类型(如 404)显示不同的信息。
4.5 路由上下文 (Route Context)
TanStack Router 允许你为路由定义一个上下文,这个上下文可以在所有子路由中访问。这对于传递全局信息或父级数据非常有用。
例如,我们可以在根路由中定义一个用户上下文。
-
修改
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()`:在任何子路由组件或其子组件中,都可以通过这个钩子访问上下文数据。
* -
在子路由中使用上下文
现在,你可以在/dashboard.tsx或其他任何子路由中访问isAuthenticated和currentUser。“`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: () =>
,
})
“`
这极大地减少了应用的初始加载时间,提升了首次内容绘制 (FCP) 和用户体验。
5.2 预加载 (Prefetching)
TanStack Router 具有智能的预加载机制:
* 悬停预加载: 当用户鼠标悬停在 <Link> 组件上时,TanStack Router 会自动预加载该链接指向路由的所有 loader 数据,甚至可以预加载路由组件本身。这使得用户点击链接后的导航几乎是瞬时的。
* 手动预加载: 虽然自动预加载已经足够智能,但你也可以通过 router.preloadRoute('/some-path') 等 API 进行手动预加载。
5.3 错误边界 (Error Boundaries)
除了前面提到的 errorComponent 可以在路由级别处理 loader 抛出的错误外,你也可以在路由树的更上层定义错误边界。
* createRootRoute 的 errorComponent: 用于捕获所有子路由未处理的错误。
* 父路由的 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 会自动是 string 或 number (如果你用 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.tsx 或 main.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 之旅吧!