TanStack Router 支持许多强大的路由概念,使你能够轻松构建复杂且动态的路由系统。
每一个概念都非常实用且强大,我们将在这个章节深入探讨它们。
路由的剖析
除了 根路由(Root Route) 之外,所有其他路由都是使用 createFileRoute 函数配置的,这在基于文件的路由中提供了类型安全:
TypeScript
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: PostsComponent,
})
createFileRoute 函数接受一个参数,即作为字符串的文件路由路径。
❓❓❓ "等等,你要我把路由文件的路径传给 createFileRoute?"
是的!但别担心,这个路径是由路由器通过 TanStack Router Bundler Plugin 或 Router CLI 为你自动写入和管理的。 因此,当你创建新路由、移动路由或重命名路由时,路径会自动为你更新。
需要这个路径名的原因完全在于 TanStack Router 神奇的类型安全机制。如果没有这个路径名,TypeScript 根本不知道我们在哪个文件里!(我们希望 TypeScript 对此有内置支持,但目前还没有 🤷♂️)
根路由 (The Root Route)
根路由是整个树中最顶层的路由,并将所有其他路由作为子路由封装起来。
-
它没有路径
-
它总是被匹配
-
它的
component总是被渲染
即使它没有路径,根路由也可以访问与其他路由相同的所有功能,包括:
-
组件 (components)
-
加载器 (loaders)
-
搜索参数验证 (search param validation)
-
等等
要创建一个根路由,请调用 createRootRoute() 函数并将其作为 Route 变量在你的路由文件中导出:
TypeScript
// 标准根路由
import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute()
// 带 Context 的根路由
import { createRootRouteWithContext } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
export interface MyRouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<MyRouterContext>()
要了解更多关于 TanStack Router 中的 Context,请参阅 路由上下文 (Router Context) 指南。
基础路由 (Basic Routes)
基础路由匹配特定的路径,例如 /about、/settings、/settings/notifications 都是基础路由,因为它们精确匹配路径。
让我们看一个 /about 路由:
TypeScript
// about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>About</div>
}
基础路由简单直接。它们精确匹配路径并渲染提供的组件。
索引路由 (Index Routes)
索引路由专门用于当父路由被精确匹配且没有子路由被匹配的情况。
让我们看一个针对 /posts URL 的索引路由:
TypeScript
// posts.index.tsx
import { createFileRoute } from '@tanstack/react-router'
// 注意末尾的斜杠,它用于定位索引路由
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
function PostsIndexComponent() {
return <div>Please select a post!</div>
}
当 URL 精确为 /posts 时,此路由将被匹配。
动态路由片段 (Dynamic Route Segments)
以 $ 开头并跟随一个标签的路由路径片段是动态的,会将 URL 的该部分捕获到 params 对象中,供你的应用程序使用。例如,路径名 /posts/123 将匹配 /posts/$postId 路由,并且 params 对象将是 { postId: '123' }。
这些参数随后可以在你的路由配置和组件中使用!让我们看一个 posts.$postId.tsx 路由:
TypeScript
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// 在 loader 中使用
loader: ({ params }) => fetchPost(params.postId),
// 或者在组件中使用
component: PostComponent,
})
function PostComponent() {
// 在组件中!
const { postId } = Route.useParams()
return <div>Post ID: {postId}</div>
}
🧠 动态片段在路径的每个 片段都能工作。例如,你可以有一个路径为
/posts/$postId/$revisionId的路由,每个$片段都会被捕获到params对象中。
Splat / 捕获所有 (Catch-All) 路由
路径仅为 $ 的路由被称为 "splat" 路由,因为它 总是 捕获从 $ 开始到结束的 URL 路径名的 任何 剩余部分。捕获的路径名随后可在 params 对象中的特殊 _splat 属性下获得。
例如,针对 files/$ 路径的路由就是一个 splat 路由。如果 URL 路径名是 /files/documents/hello-world,则 params 对象将在特殊 _splat 属性下包含 documents/hello-world:
JavaScript
{
'_splat': 'documents/hello-world'
}
⚠️ 在路由器的 v1 版本中,为了向后兼容,splat 路由也可以用
*代替_splat键来表示。这将在 v2 中移除。
🧠 为什么要用$?多亏了像 Remix 这样的工具,我们知道尽管*是表示通配符的最常见字符,但它们与文件名或 CLI 工具的配合并不好,所以就像他们一样,我们决定使用$代替。
可选路径参数 (Optional Path Parameters)
可选路径参数允许你定义 URL 中可能存在也可能不存在的路由片段。它们使用 {-$paramName} 语法,并提供灵活的路由模式,其中某些参数是可选的。
TypeScript
// posts.{-$category}.tsx - 可选的 category 参数
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/{-$category}')({
component: PostsComponent,
})
function PostsComponent() {
const { category } = Route.useParams()
return <div>{category ? `Posts in ${category}` : 'All Posts'}</div>
}
此路由将同时匹配 /posts(category 为 undefined)和 /posts/tech(category 为 "tech")。
你也可以在单个路由中定义多个可选参数:
TypeScript
// posts.{-$category}.{-$slug}.tsx
export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({
component: PostsComponent,
})
此路由匹配 /posts、/posts/tech 和 /posts/tech/hello-world。
🧠 带有可选参数的路由优先级低于精确匹配,确保像
/posts/featured这样更具体的路由会在/posts/{-$category}之前被匹配。
布局路由 (Layout Routes)
布局路由用于使用额外的组件和逻辑包裹子路由。它们通常用于:
-
用布局组件包裹子路由
-
在显示任何子路由之前强制执行
loader要求 -
验证并向子路由提供搜索参数 (search params)
-
为子路由提供错误组件或挂起 (pending) 元素的回退 (fallbacks)
-
向所有子路由提供共享上下文 (context)
-
等等!
让我们看一个名为 app.tsx 的布局路由示例:
routes/
├── app.tsx
├── app.dashboard.tsx
├── app.settings.tsx
在上面的树中,app.tsx 是一个布局路由,它包裹了两个子路由:app.dashboard.tsx 和 app.settings.tsx。
这种树结构用于用布局组件包裹子路由:
TypeScript
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app')({
component: AppLayoutComponent,
})
function AppLayoutComponent() {
return (
<div>
<h1>App Layout</h1>
<Outlet />
</div>
)
}
下表显示了基于 URL 将渲染哪些组件:
| URL Path | Component |
|---|---|
/app |
<AppLayout> |
/app/dashboard |
<AppLayout><Dashboard> |
/app/settings |
<AppLayout><Settings> |
由于 TanStack Router 支持混合扁平路由和目录路由,你也可以在目录中使用布局路由来表达应用程序的路由:
routes/
├── app/
│ ├── route.tsx
│ ├── dashboard.tsx
│ ├── settings.tsx
在这个嵌套树中,app/route.tsx 文件是布局路由的配置,它包裹了两个子路由:app/dashboard.tsx 和 app/settings.tsx。
布局路由还允许你对动态路由片段强制执行组件和加载器逻辑:
bash
routes/
├── app/users/
│ ├── $userId/
| | ├── route.tsx
| | ├── index.tsx
| | ├── edit.tsx
无路径布局路由 (Pathless Layout Routes)
像 布局路由 一样,无路径布局路由用于使用额外的组件和逻辑包裹子路由。但是,无路径布局路由不需要在 URL 中有匹配的 path,它们用于包裹子路由而无需 URL 路径匹配。
无路径布局路由以前划线 (_) 为前缀,表示它们是"无路径"的。
🧠
_前缀之后的部分用作路由的 ID,这是必需的,因为每个路由必须是唯一可识别的,特别是在使用 TypeScript 时,以避免类型错误并有效地实现自动完成。
让我们看一个名为 _pathlessLayout.tsx 的路由示例:
routes/
├── _pathlessLayout.tsx
├── _pathlessLayout.a.tsx
├── _pathlessLayout.b.tsx
在上面的树中,_pathlessLayout.tsx 是一个无路径布局路由,它包裹了两个子路由:_pathlessLayout.a.tsx 和 _pathlessLayout.b.tsx。
_pathlessLayout.tsx 路由用于用无路径布局组件包裹子路由:
TypeScript
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_pathlessLayout')({
component: PathlessLayoutComponent,
})
function PathlessLayoutComponent() {
return (
<div>
<h1>Pathless layout</h1>
<Outlet />
</div>
)
}
下表显示了基于 URL 将渲染哪个组件:
| URL Path | Component |
|---|---|
/ |
<Index> |
/a |
<PathlessLayout><A> |
/b |
<PathlessLayout><B> |
由于 TanStack Router 支持混合扁平路由和目录路由,你也可以在目录中使用无路径布局路由来表达应用程序的路由:
css
routes/
├── _pathlessLayout/
│ ├── route.tsx
│ ├── a.tsx
│ ├── b.tsx
但是,与布局路由不同,由于无路径布局路由不基于 URL 路径片段进行匹配,这意味着这些路由不支持将 动态路由片段 作为其路径的一部分,因此无法在 URL 中进行匹配。
这意味着你不能这样做:
bash
routes/
├── _$postId/ ❌
│ ├── ...
相反,你必须这样做:
bash
routes/
├── $postId/
├── _postPathlessLayout/ ✅
│ ├── ...
非嵌套路由 (Non-Nested Routes)
非嵌套路由可以通过在父文件路由片段后加上 _ 后缀来创建,用于将路由从其父级中取消嵌套 (un-nest) 并渲染其自己的组件树。
考虑以下的扁平路由树:
bash
routes/
├── posts.tsx
├── posts.$postId.tsx
├── posts_.$postId.edit.tsx
下表显示了基于 URL 将渲染哪个组件:
| URL Path | Component |
|---|---|
/posts |
<Posts> |
/posts/123 |
<Posts><Post postId="123"> |
/posts/123/edit |
<PostEditor postId="123"> |
-
posts.$postId.tsx路由正常嵌套在posts.tsx路由下,将渲染<Posts><Post>。 -
posts_.$postId.edit.tsx路由与其他路由不共享 相同的posts前缀,因此将被视为顶层路由,并将渲染<PostEditor>。
!NOTE
虽然在基于文件的路由中使用非嵌套路由已经非常出色,但在某些条件下可能会出现异常行为。
其中许多限制已经在下一主要版本的 TanStack Router 中得到解决并将发布。
要尽早享受这些好处,你可以在路由器插件配置中启用实验性的
nonNestedRoutes标志:
TypeScriptexport default defineConfig({ plugins: [ tanstackRouter({ // some config, experimental: { nonNestedRoutes: true, }, }), ], })重要提示:这确实会导致在 useParams、useNavigate 等中引用非嵌套路由的方式发生轻微变化。因此,这已作为功能标志发布。
路径中不再预期包含尾随下划线:
以前:
TypeScriptuseParams({ from: '/posts_/$postId/edit' })现在:
TypeScriptuseParams({ from: '/posts/$postId/edit' })
从路由中排除文件和文件夹
可以通过在文件名后附加 - 前缀来将文件和文件夹从路由生成中排除。这使你能够在路由目录中共存逻辑代码。
考虑以下的路由树:
less
routes/
├── posts.tsx
├── -posts-table.tsx // 👈🏼 被忽略
├── -components/ // 👈🏼 被忽略
│ ├── header.tsx // 👈🏼 被忽略
│ ├── footer.tsx // 👈🏼 被忽略
│ ├── ...
我们可以从排除的文件中导入内容到我们的 posts 路由中:
TypeScript
import { createFileRoute } from '@tanstack/react-router'
import { PostsTable } from './-posts-table'
import { PostsHeader } from './-components/header'
import { PostsFooter } from './-components/footer'
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
component: PostComponent,
})
function PostComponent() {
const posts = Route.useLoaderData()
return (
<div>
<PostsHeader />
<PostsTable posts={posts} />
<PostsFooter />
</div>
)
}
排除的文件不会被添加到 routeTree.gen.ts 中。
无路径路由组目录 (Pathless Route Group Directories)
无路径路由组目录使用 () 作为一种将路由文件分组的方式,无论其路径如何。它们纯粹是组织性的,不会以任何方式影响路由树或组件树。
scss
routes/
├── index.tsx
├── (app)/
│ ├── dashboard.tsx
│ ├── settings.tsx
│ ├── users.tsx
├── (auth)/
│ ├── login.tsx
│ ├── register.tsx
在上面的示例中,app 和 auth 目录纯粹是组织性的,不会以任何方式影响路由树或组件树。它们用于将相关的路由分组在一起,以便于导航和组织。
下表显示了基于 URL 将渲染哪个组件:
| URL Path | Component |
|---|---|
/ |
<Index> |
/dashboard |
<Dashboard> |
/settings |
<Settings> |
/users |
<Users> |
/login |
<Login> |
/register |
<Register> |
如你所见,app 和 auth 目录纯粹是组织性的,不会以任何方式影响路由树或组件树。