TanStack Router 支持许多强大的路由概念,让您能够轻松构建复杂和动态的路由系统。
这些概念都很有用且强大,我们将在以下部分深入探讨每一个概念。
除了根路由之外的所有其他路由,都使用 createFileRoute 函数进行配置,该函数在使用基于文件的路由时提供类型安全:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: PostsComponent,
})
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: PostsComponent,
})
createFileRoute 函数接受一个参数,即文件路由的路径字符串。
❓❓❓ "等等,您让我将路由文件的路径传递给 createFileRoute?"
是的!但不要担心,这个路径是通过 TanStack Router 打包器插件或 Router CLI 自动为您编写和管理的。 因此,当您创建新路由、移动路由或重命名路由时,路径将自动为您更新。
这个路径名的原因与 TanStack Router 神奇的类型安全有关。没有这个路径名,TypeScript 就不知道我们在哪个文件中!(我们希望 TypeScript 有内置功能,但他们还没有 🤷♂️)
根路由是整个树中最顶层的路由,将所有其他路由封装为子路由。
尽管它没有路径,根路由可以访问与其他路由相同的所有功能,包括:
要创建根路由,调用 createRootRoute() 函数并在路由文件中将其导出为 Route 变量:
// Standard root route
import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute()
// Root route with Context
import { createRootRouteWithContext } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
export interface MyRouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<MyRouterContext>()
// Standard root route
import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute()
// Root route with 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 中上下文的信息,请参阅路由上下文指南。
基本路由匹配特定路径,例如 /about、/settings、/settings/notifications 都是基本路由,因为它们完全匹配路径。
让我们看一个 /about 路由:
// about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>About</div>
}
// about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>About</div>
}
基本路由简单直接。它们完全匹配路径并渲染提供的组件。
索引路由专门针对其父路由,当父路由完全匹配且没有子路由匹配时。
让我们看一个 /posts URL 的索引路由:
// posts.index.tsx
import { createFileRoute } from '@tanstack/react-router'
// Note the trailing slash, which is used to target index routes
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
function PostsIndexComponent() {
return <div>Please select a post!</div>
}
// posts.index.tsx
import { createFileRoute } from '@tanstack/react-router'
// Note the trailing slash, which is used to target index routes
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
function PostsIndexComponent() {
return <div>Please select a post!</div>
}
当 URL 完全是 /posts 时,此路由将被匹配。
以 $ 开头后跟标签的路由路径段是动态的,会将 URL 的该部分捕获到 params 对象中供您的应用程序使用。例如,路径名 /posts/123 将匹配 /posts/$postId 路由,params 对象将是 { postId: '123' }。
这些参数然后可以在您的路由配置和组件中使用!让我们看一个 posts.$postId.tsx 路由:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// In a loader
loader: ({ params }) => fetchPost(params.postId),
// Or in a component
component: PostComponent,
})
function PostComponent() {
// 在组件中!
const { postId } = Route.useParams()
return <div>Post ID: {postId}</div>
}
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// In a loader
loader: ({ params }) => fetchPost(params.postId),
// Or in a component
component: PostComponent,
})
function PostComponent() {
// 在组件中!
const { postId } = Route.useParams()
return <div>Post ID: {postId}</div>
}
🧠 动态段在路径的每个段都有效。例如,您可以有一个路径为 /posts/$postId/$revisionId 的路由,每个 $ 段都会被捕获到 params 对象中。
路径仅为 $ 的路由称为"通配符"路由,因为它_总是_捕获从 $ 到末尾的 URL 路径名的_任何_剩余部分。捕获的路径名然后在 params 对象中的特殊 _splat 属性下可用。
例如,针对 files/$ 路径的路由是通配符路由。如果 URL 路径名是 /files/documents/hello-world,params 对象将在特殊的 _splat 属性下包含 documents/hello-world:
{
'_splat': 'documents/hello-world'
}
{
'_splat': 'documents/hello-world'
}
⚠️ 在路由器的 v1 版本中,通配符路由也用 * 而不是 _splat 键表示,以保持向后兼容性。这将在 v2 中删除。
🧠 为什么使用 $?感谢像 Remix 这样的工具,我们知道尽管 * 是表示通配符最常见的字符,但它们与文件名或 CLI 工具不兼容,所以就像它们一样,我们决定使用 $ 代替。
布局路由用于使用额外的组件和逻辑包装子路由。它们对以下方面很有用:
让我们看一个名为 app.tsx 的示例布局路由:
routes/
├── app.tsx
├── app.dashboard.tsx
├── app.settings.tsx
routes/
├── app.tsx
├── app.dashboard.tsx
├── app.settings.tsx
在上面的树中,app.tsx 是一个布局路由,包装了两个子路由:app.dashboard.tsx 和 app.settings.tsx。
这个树结构用于使用布局组件包装子路由:
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app')({
component: AppLayoutComponent,
})
function AppLayoutComponent() {
return (
<div>
<h1>App Layout</h1>
<Outlet />
</div>
)
}
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
routes/
├── app/
│ ├── route.tsx
│ ├── dashboard.tsx
│ ├── settings.tsx
在这个嵌套树中,app/route.tsx 文件是布局路由的配置,包装了两个子路由:app/dashboard.tsx 和 app/settings.tsx。
布局路由还允许您为动态路由段强制执行组件和加载器逻辑:
routes/
├── app/users/
│ ├── $userId/
| | ├── route.tsx
| | ├── index.tsx
| | ├── edit.tsx
routes/
├── app/users/
│ ├── $userId/
| | ├── route.tsx
| | ├── index.tsx
| | ├── edit.tsx
与布局路由一样,无路径布局路由用于使用额外的组件和逻辑包装子路由。但是,无路径布局路由不需要 URL 中的匹配 path,用于在不需要 URL 中匹配 path 的情况下使用额外的组件和逻辑包装子路由。
无路径布局路由以下划线(_)为前缀,表示它们是"无路径"的。
🧠 _ 前缀后的路径部分用作路由的 ID,这是必需的,因为每个路由都必须是唯一可识别的,特别是在使用 TypeScript 时,以避免类型错误并有效地实现自动完成。
让我们看一个名为 _pathlessLayout.tsx 的示例路由:
routes/
├── _pathlessLayout.tsx
├── _pathlessLayout.a.tsx
├── _pathlessLayout.b.tsx
routes/
├── _pathlessLayout.tsx
├── _pathlessLayout.a.tsx
├── _pathlessLayout.b.tsx
在上面的树中,_pathlessLayout.tsx 是一个无路径布局路由,包装了两个子路由:_pathlessLayout.a.tsx 和 _pathlessLayout.b.tsx。
_pathlessLayout.tsx 路由用于使用无路径布局组件包装子路由:
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_pathlessLayout')({
component: PathlessLayoutComponent,
})
function PathlessLayoutComponent() {
return (
<div>
<h1>Pathless layout</h1>
<Outlet />
</div>
)
}
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 支持混合扁平和目录路由,您也可以在目录中使用无路径布局路由来表达应用程序的路由:
routes/
├── _pathlessLayout/
│ ├── route.tsx
│ ├── a.tsx
│ ├── b.tsx
routes/
├── _pathlessLayout/
│ ├── route.tsx
│ ├── a.tsx
│ ├── b.tsx
但是,与布局路由不同,由于无路径布局路由不基于 URL 路径段进行匹配,这意味着这些路由不支持动态路由段作为其路径的一部分,因此无法在 URL 中匹配。
这意味着您不能这样做:
routes/
├── _$postId/ ❌
│ ├── ...
routes/
├── _$postId/ ❌
│ ├── ...
相反,您必须这样做:
routes/
├── $postId/
├── _postPathlessLayout/ ✅
│ ├── ...
routes/
├── $postId/
├── _postPathlessLayout/ ✅
│ ├── ...
非嵌套路由可以通过在父文件路由段后缀加上 _ 来创建,用于从其父路由中取消嵌套路由并渲染其自己的组件树。
考虑以下扁平路由树:
routes/
├── posts.tsx
├── posts.$postId.tsx
├── posts_.$postId.edit.tsx
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"> |
可以通过在文件名前添加 - 前缀来从路由生成中排除文件和文件夹。这使您能够在路由目录中共置逻辑。
考虑以下路由树:
routes/
├── posts.tsx
├── -posts-table.tsx // 👈🏼 ignored
├── -components/ // 👈🏼 ignored
│ ├── header.tsx // 👈🏼 ignored
│ ├── footer.tsx // 👈🏼 ignored
│ ├── ...
routes/
├── posts.tsx
├── -posts-table.tsx // 👈🏼 ignored
├── -components/ // 👈🏼 ignored
│ ├── header.tsx // 👈🏼 ignored
│ ├── footer.tsx // 👈🏼 ignored
│ ├── ...
我们可以从排除的文件导入到我们的 posts 路由中
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>
)
}
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 中。
无路径路由组目录使用 () 作为一种将路由文件组合在一起的方式,无论其路径如何。它们纯粹是组织性的,不会以任何方式影响路由树或组件树。
routes/
├── index.tsx
├── (app)/
│ ├── dashboard.tsx
│ ├── settings.tsx
│ ├── users.tsx
├── (auth)/
│ ├── login.tsx
│ ├── register.tsx
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 目录纯粹是组织性的,不会以任何方式影响路由树或组件树。