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 Bundler Plugin 或 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() {
// In a component!
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() {
// In a component!
const { postId } = Route.useParams()
return <div>Post ID: {postId}</div>
}
🧠 动态段在路径的**每个**段都起作用。例如,您可以有一个路径为/posts/$postId/$revisionId的路由,并且每个$段都将被捕获到params对象中。
路径仅为$的路由被称为“通配符”路由,因为它**总是**捕获从$到末尾的 URL 路径名的**任何**剩余部分。捕获的路径名随后在params对象中以特殊的_splat属性提供。
例如,一个目标为files/$路径的路由是一个通配符路由。如果 URL 路径名为/files/documents/hello-world,则params对象将包含documents/hello-world在特殊的_splat属性下。
{
'_splat': 'documents/hello-world'
}
{
'_splat': 'documents/hello-world'
}
⚠️ 在路由器的 v1 版本中,通配符路由也用*表示,而不是_splat键,以保持向后兼容性。这将在 v2 中移除。
🧠 为什么要用$?得益于 Remix 等工具,我们知道尽管*是表示通配符最常见的字符,但它们与文件名或 CLI 工具不兼容,所以就像它们一样,我们决定改用$。
可选路径参数允许您定义 URL 中可能存在或不存在的路由段。它们使用{-$paramName}语法,提供灵活的路由模式,其中某些参数是可选的。
// posts.{-$category}.tsx - Optional category parameter
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}.tsx - Optional category parameter
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")。
您还可以在单个路由中定义多个可选参数。
// posts.{-$category}.{-$slug}.tsx
export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({
component: PostsComponent,
})
// posts.{-$category}.{-$slug}.tsx
export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({
component: PostsComponent,
})
此路由匹配/posts、/posts/tech和/posts/tech/hello-world。
🧠 带有可选参数的路由优先级低于精确匹配的路由,这确保了诸如/posts/featured之类的更具体路由在/posts/{-$category}之前被匹配。
布局路由用于用额外的组件和逻辑封装子路由。它们对于以下情况很有用:
让我们看看一个名为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 路径 | 组件 |
---|---|
/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 路径 | 组件 |
---|---|
/ | <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 路径 | 组件 |
---|---|
/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 路径 | 组件 |
---|---|
/ | <Index> |
/dashboard | <Dashboard> |
/settings | <Settings> |
/users | <Users> |
/login | <Login> |
/register | <Register> |
如您所见,app和auth目录纯粹是为了组织,它们不会以任何方式影响路由树或组件树。
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。