路由概念

TanStack Router 支持许多强大的路由概念,让您可以轻松构建复杂且动态的路由系统。

这些概念中的每一个都既有用又强大,我们将在接下来的章节中深入探讨它们。

路由的构成

根路由 外,所有其他路由都使用 createFileRoute 函数进行配置,该函数在使用基于文件的路由时提供了类型安全。

tsx
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/')({
  component: PostsComponent,
})
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/')({
  component: PostsComponent,
})

createFileRoute 函数接受一个参数,即文件路由的路径(字符串)。

❓❓❓ “等等,你让我把路由文件的路径传给 createFileRoute?”

是的!但别担心,这个路径是由路由器通过 TanStack Router Bundler 插件或 Router CLI **自动为您编写和管理的**。因此,当您创建新路由、移动路由或重命名路由时,路径都会自动为您更新。

这个路径名与 TanStack Router 的神奇类型安全有关。没有这个路径名,TypeScript 就无法知道我们在哪个文件!(我们希望 TypeScript 内置这个功能,但它们还没有 🤷‍♂️)

根路由

根路由是整个树中最顶层的路由,并将所有其他路由封装为子路由。

  • 它没有路径。
  • 它**总是**被匹配。
  • 它的 component **总是**被渲染。

即使它没有路径,根路由也可以访问与其他路由相同的功能,包括:

  • 组件
  • 加载器
  • 搜索参数验证
  • 等等。

要创建根路由,请调用 createRootRoute() 函数,并将其作为 Route 变量在您的路由文件中导出。

tsx
// Standard root route
import { createRootRoute } from '@tanstack/solid-router'

export const Route = createRootRoute()

// Root route with Context
import { createRootRouteWithContext } from '@tanstack/solid-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/solid-router'

export const Route = createRootRoute()

// Root route with Context
import { createRootRouteWithContext } from '@tanstack/solid-router'
import type { QueryClient } from '@tanstack/react-query'

export interface MyRouterContext {
  queryClient: QueryClient
}
export const Route = createRootRouteWithContext<MyRouterContext>()

要了解有关 TanStack Router 中 Context 的更多信息,请参阅 Router Context 指南。

基本路由

基本路由匹配特定路径,例如 /about/settings/settings/notifications,因为它们会精确匹配路径。

让我们来看看一个 /about 路由。

tsx
// about.tsx
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/about')({
  component: AboutComponent,
})

function AboutComponent() {
  return <div>About</div>
}
// about.tsx
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/about')({
  component: AboutComponent,
})

function AboutComponent() {
  return <div>About</div>
}

基本路由简单明了。它们会精确匹配路径并渲染提供的组件。

索引路由

索引路由专门针对父路由,当父路由被**精确匹配且没有子路由被匹配**时。

让我们来看看 /posts URL 的索引路由。

tsx
// posts.index.tsx
import { createFileRoute } from '@tanstack/solid-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/solid-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 路由。

tsx
import { createFileRoute } from '@tanstack/solid-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/solid-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 对象中。

通配符 / 捕获所有路由

仅包含 $ 的路由被称为“splat”路由,因为它**总是**捕获 URL 路径名中从 $ 到末尾的**任何**剩余部分。然后,捕获的路径名可在 params 对象中以特殊的 _splat 属性的形式提供。

例如,一个针对 files/$ 路径的路由是一个 splat 路由。如果 URL 路径名为 /files/documents/hello-world,则 params 对象将在特殊的 _splat 属性下包含 documents/hello-world

js
{
  '_splat': 'documents/hello-world'
}
{
  '_splat': 'documents/hello-world'
}

⚠️ 在路由 v1 中,为了向后兼容,splat 路由也用 * 而非 _splat 键来表示。这将在 v2 中移除。

🧠 为什么使用 $?得益于 Remix 等工具,我们知道尽管 * 是表示通配符的最常见字符,但它们与文件名或 CLI 工具不太兼容,因此我们像它们一样决定使用 $

可选路径参数

可选路径参数允许您定义 URL 中可能存在或不存在的路由段。它们使用 {-$paramName} 语法,并提供灵活的路由模式,其中某些参数是可选的。

tsx
// posts.{-$category}.tsx - Optional category parameter
import { createFileRoute } from '@tanstack/solid-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/solid-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")。

您还可以在单个路由中定义多个可选参数。

tsx
// 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} 之前被匹配。

布局路由

布局路由用于使用其他组件和逻辑包装子路由。它们对于以下方面很有用:

  • 使用布局组件包装子路由
  • 在显示任何子路由之前强制执行 loader 要求
  • 为子路由验证和提供搜索参数
  • 为子路由提供错误组件或待处理元素的备用方案
  • 为所有子路由提供共享上下文
  • 等等!

让我们来看看一个名为 app.tsx 的示例布局路由。

routes/
├── app.tsx
├── app.dashboard.tsx
├── app.settings.tsx
routes/
├── app.tsx
├── app.dashboard.tsx
├── app.settings.tsx

在上面的树中,app.tsx 是一个布局路由,它包装了两个子路由:app.dashboard.tsxapp.settings.tsx

此树结构用于使用布局组件包装子路由。

tsx
import { Outlet, createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/app')({
  component: AppLayoutComponent,
})

function AppLayoutComponent() {
  return (
    <div>
      <h1>App Layout</h1>
      <Outlet />
    </div>
  )
}
import { Outlet, createFileRoute } from '@tanstack/solid-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.tsxapp/settings.tsx

布局路由还允许您强制执行动态路由段的组件和加载器逻辑。

routes/
├── app/users/
│   ├── $userId/
|   |   ├── route.tsx
|   |   ├── index.tsx
|   |   ├── edit.tsx
routes/
├── app/users/
│   ├── $userId/
|   |   ├── route.tsx
|   |   ├── index.tsx
|   |   ├── edit.tsx

无路径布局路由

与布局路由 (Layout Routes) 类似,无路径布局路由用于使用其他组件和逻辑包装子路由。但是,无路径布局路由不需要 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 路由用于使用无路径布局组件包装子路由。

tsx
import { Outlet, createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/_pathlessLayout')({
  component: PathlessLayoutComponent,
})

function PathlessLayoutComponent() {
  return (
    <div>
      <h1>Pathless layout</h1>
      <Outlet />
    </div>
  )
}
import { Outlet, createFileRoute } from '@tanstack/solid-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">
  • posts.$postId.tsx 路由像往常一样嵌套在 posts.tsx 路由下,并将渲染 <Posts><Post>
  • posts_.$postId.edit.tsx 路由**不共享**与其他路由相同的 posts 前缀,因此它将被视为顶级路由,并将渲染 <PostEditor>

从路由中排除文件和文件夹

文件和文件夹可以通过在文件名中添加 - 前缀来从路由生成中排除。这使您能够将逻辑放在路由目录中。

考虑以下路由树:

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 路由中。

tsx
import { createFileRoute } from '@tanstack/solid-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/solid-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

在上面的示例中,appauth 目录仅用于组织目的,不会以任何方式影响路由树或组件树。它们用于将相关路由分组在一起,以便于导航和组织。

下表显示了将根据 URL 渲染哪些组件:

URL 路径组件
/<Index>
/dashboard<Dashboard>
/settings<Settings>
/users<Users>
/login<Login>
/register<Register>

正如您所见,appauth 目录仅用于组织目的,不会以任何方式影响路由树或组件树。

我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。