框架
版本

虚拟文件路由

我们感谢 Remix 团队开创了虚拟文件路由的概念。我们从中汲取灵感,并将其与 TanStack Router 现有的基于文件的路由树生成相结合。

虚拟文件路由是一个强大的概念,它允许您使用引用项目中真实文件的代码以编程方式构建路由树。这在以下情况下很有用:

  • 您有一个想要保留的现有路由组织。
  • 您想要自定义路由文件的位置。
  • 您想要完全覆盖 TanStack Router 的基于文件的路由生成,并构建自己的约定。

以下是一个使用虚拟文件路由将路由树映射到项目中一组真实文件的快速示例:

tsx
// routes.ts
import {
  rootRoute,
  route,
  index,
  layout,
  physical,
} from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  index('index.tsx'),
  layout('pathlessLayout.tsx', [
    route('/dashboard', 'app/dashboard.tsx', [
      index('app/dashboard-index.tsx'),
      route('/invoices', 'app/dashboard-invoices.tsx', [
        index('app/invoices-index.tsx'),
        route('$id', 'app/invoice-detail.tsx'),
      ]),
    ]),
    physical('/posts', 'posts'),
  ]),
])
// routes.ts
import {
  rootRoute,
  route,
  index,
  layout,
  physical,
} from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  index('index.tsx'),
  layout('pathlessLayout.tsx', [
    route('/dashboard', 'app/dashboard.tsx', [
      index('app/dashboard-index.tsx'),
      route('/invoices', 'app/dashboard-invoices.tsx', [
        index('app/invoices-index.tsx'),
        route('$id', 'app/invoice-detail.tsx'),
      ]),
    ]),
    physical('/posts', 'posts'),
  ]),
])

配置

虚拟文件路由可以通过以下方式配置:

  • Vite/Rspack/Webpack 的 TanStackRouter 插件
  • TanStack Router CLI 的 tsr.config.json 文件

通过 TanStackRouter 插件配置

如果您正在使用 Vite/Rspack/Webpack 的 TanStackRouter 插件,您可以通过在设置插件时将路由文件的路径传递给 virtualRoutesConfig 选项来配置虚拟文件路由:

tsx
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      virtualRouteConfig: './routes.ts',
    }),
    react(),
  ],
})
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      virtualRouteConfig: './routes.ts',
    }),
    react(),
  ],
})

或者,您可以选择直接在配置中定义虚拟路由:

tsx
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import { rootRoute } from '@tanstack/virtual-file-routes'

const routes = rootRoute('root.tsx', [
  // ... the rest of your virtual route tree
])

export default defineConfig({
  plugins: [tanstackRouter({ virtualRouteConfig: routes }), react()],
})
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import { rootRoute } from '@tanstack/virtual-file-routes'

const routes = rootRoute('root.tsx', [
  // ... the rest of your virtual route tree
])

export default defineConfig({
  plugins: [tanstackRouter({ virtualRouteConfig: routes }), react()],
})

创建虚拟文件路由

要创建虚拟文件路由,您需要导入 @tanstack/virtual-file-routes 包。此包提供了一组函数,允许您创建引用项目中真实文件的虚拟路由。该包导出了几个实用函数:

  • rootRoute - 创建一个虚拟根路由。
  • route - 创建一个虚拟路由。
  • index - 创建一个虚拟索引路由。
  • layout - 创建一个虚拟无路径布局路由。
  • physical - 创建一个物理虚拟路由(稍后会详细介绍)。

虚拟根路由

rootRoute 函数用于创建虚拟根路由。它接受一个文件名和一个子路由数组。以下是一个虚拟根路由的示例:

tsx
// routes.ts
import { rootRoute } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  // ... children routes
])
// routes.ts
import { rootRoute } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  // ... children routes
])

虚拟路由

route 函数用于创建虚拟路由。它接受一个路径、一个文件名和一个子路由数组。以下是一个虚拟路由的示例:

tsx
// routes.ts
import { route } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  route('/about', 'about.tsx', [
    // ... children routes
  ]),
])
// routes.ts
import { route } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  route('/about', 'about.tsx', [
    // ... children routes
  ]),
])

您也可以不带文件名地定义虚拟路由。这允许为其子路由设置一个共同的路径前缀:

tsx
// routes.ts
import { route } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  route('/hello', [
    route('/world', 'world.tsx'), // full path will be "/hello/world"
    route('/universe', 'universe.tsx'), // full path will be "/hello/universe"
  ]),
])
// routes.ts
import { route } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  route('/hello', [
    route('/world', 'world.tsx'), // full path will be "/hello/world"
    route('/universe', 'universe.tsx'), // full path will be "/hello/universe"
  ]),
])

虚拟索引路由

index 函数用于创建虚拟索引路由。它接受一个文件名。以下是一个虚拟索引路由的示例:

tsx
import { index } from '@tanstack/virtual-file-routes'

const routes = rootRoute('root.tsx', [index('index.tsx')])
import { index } from '@tanstack/virtual-file-routes'

const routes = rootRoute('root.tsx', [index('index.tsx')])

虚拟无路径路由

layout 函数用于创建虚拟无路径路由。它接受一个文件名、一个子路由数组和一个可选的无路径 ID。以下是一个虚拟无路径路由的示例:

tsx
// routes.ts
import { layout } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  layout('pathlessLayout.tsx', [
    // ... children routes
  ]),
])
// routes.ts
import { layout } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  layout('pathlessLayout.tsx', [
    // ... children routes
  ]),
])

您还可以指定一个无路径 ID,为路由提供一个与文件名不同的唯一标识符:

tsx
// routes.ts
import { layout } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  layout('my-pathless-layout-id', 'pathlessLayout.tsx', [
    // ... children routes
  ]),
])
// routes.ts
import { layout } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  layout('my-pathless-layout-id', 'pathlessLayout.tsx', [
    // ... children routes
  ]),
])

物理虚拟路由

物理虚拟路由是一种将“老式”TanStack Router 基于文件的路由约定目录“挂载”到特定 URL 路径下的方式。如果您使用虚拟路由来自定义路由树层级较高的一小部分,但希望对子路由和目录使用标准的文件路由约定,这会很有用。

考虑以下文件结构:

/routes
├── root.tsx
├── index.tsx
├── pathlessLayout.tsx
├── app
│   ├── dashboard.tsx
│   ├── dashboard-index.tsx
│   ├── dashboard-invoices.tsx
│   ├── invoices-index.tsx
│   ├── invoice-detail.tsx
└── posts
    ├── index.tsx
    ├── $postId.tsx
    ├── $postId.edit.tsx
    ├── comments/
    │   ├── index.tsx
    │   ├── $commentId.tsx
    └── likes/
        ├── index.tsx
        ├── $likeId.tsx
/routes
├── root.tsx
├── index.tsx
├── pathlessLayout.tsx
├── app
│   ├── dashboard.tsx
│   ├── dashboard-index.tsx
│   ├── dashboard-invoices.tsx
│   ├── invoices-index.tsx
│   ├── invoice-detail.tsx
└── posts
    ├── index.tsx
    ├── $postId.tsx
    ├── $postId.edit.tsx
    ├── comments/
    │   ├── index.tsx
    │   ├── $commentId.tsx
    └── likes/
        ├── index.tsx
        ├── $likeId.tsx

让我们使用虚拟路由自定义除 posts 之外的所有路由树,然后使用物理虚拟路由将 posts 目录挂载到 /posts 路径下:

tsx
// routes.ts
export const routes = rootRoute('root.tsx', [
  // Set up your virtual routes as normal
  index('index.tsx'),
  layout('pathlessLayout.tsx', [
    route('/dashboard', 'app/dashboard.tsx', [
      index('app/dashboard-index.tsx'),
      route('/invoices', 'app/dashboard-invoices.tsx', [
        index('app/invoices-index.tsx'),
        route('$id', 'app/invoice-detail.tsx'),
      ]),
    ]),
    // Mount the `posts` directory under the `/posts` path
    physical('/posts', 'posts'),
  ]),
])
// routes.ts
export const routes = rootRoute('root.tsx', [
  // Set up your virtual routes as normal
  index('index.tsx'),
  layout('pathlessLayout.tsx', [
    route('/dashboard', 'app/dashboard.tsx', [
      index('app/dashboard-index.tsx'),
      route('/invoices', 'app/dashboard-invoices.tsx', [
        index('app/invoices-index.tsx'),
        route('$id', 'app/invoice-detail.tsx'),
      ]),
    ]),
    // Mount the `posts` directory under the `/posts` path
    physical('/posts', 'posts'),
  ]),
])

TanStack Router 文件路由中的虚拟路由

上一节向您展示了如何在虚拟路由配置中使用 TanStack Router 的基于文件的路由约定。然而,反过来也是可能的。
您可以使用 TanStack Router 的基于文件的路由约定来配置应用程序路由树的主体部分,并为特定的子树选择虚拟路由配置。

考虑以下文件结构:

/routes
├── __root.tsx
├── foo
│   ├── bar
│   │   ├── __virtual.ts
│   │   ├── details.tsx
│   │   ├── home.tsx
│   │   └── route.ts
│   └── bar.tsx
└── index.tsx
/routes
├── __root.tsx
├── foo
│   ├── bar
│   │   ├── __virtual.ts
│   │   ├── details.tsx
│   │   ├── home.tsx
│   │   └── route.ts
│   └── bar.tsx
└── index.tsx

让我们看看 bar 目录,其中包含一个名为 __virtual.ts 的特殊文件。此文件指示生成器为该目录(及其子目录)切换到虚拟文件路由配置。

__virtual.ts 为路由树的特定子树配置虚拟路由。它使用与上述相同的 API,唯一的区别是该子树没有定义 rootRoute

tsx
// routes/foo/bar/__virtual.ts
import {
  defineVirtualSubtreeConfig,
  index,
  route,
} from '@tanstack/virtual-file-routes'

export default defineVirtualSubtreeConfig([
  index('home.tsx'),
  route('$id', 'details.tsx'),
])
// routes/foo/bar/__virtual.ts
import {
  defineVirtualSubtreeConfig,
  index,
  route,
} from '@tanstack/virtual-file-routes'

export default defineVirtualSubtreeConfig([
  index('home.tsx'),
  route('$id', 'details.tsx'),
])

辅助函数 defineVirtualSubtreeConfig 紧密模仿了 vite 的 defineConfig,并允许您通过默认导出定义子树配置。默认导出可以是:

  • 一个子树配置对象
  • 一个返回子树配置对象的函数
  • 一个返回子树配置对象的异步函数

盗梦空间

您可以随意混合搭配 TanStack Router 的基于文件的路由约定和虚拟路由配置。
让我们深入探讨!
查看以下示例,该示例首先使用基于文件的路由约定,然后切换到 /posts 的虚拟路由配置,然后切换回 /posts/lets-go 的基于文件的路由约定,然后再次切换到 /posts/lets-go/deeper 的虚拟路由配置。

├── __root.tsx
├── index.tsx
├── posts
│   ├── __virtual.ts
│   ├── details.tsx
│   ├── home.tsx
│   └── lets-go
│       ├── deeper
│       │   ├── __virtual.ts
│       │   └── home.tsx
│       └── index.tsx
└── posts.tsx
├── __root.tsx
├── index.tsx
├── posts
│   ├── __virtual.ts
│   ├── details.tsx
│   ├── home.tsx
│   └── lets-go
│       ├── deeper
│       │   ├── __virtual.ts
│       │   └── home.tsx
│       └── index.tsx
└── posts.tsx

通过 TanStack Router CLI 配置

如果您正在使用 TanStack Router CLI,您可以通过在 tsr.config.json 文件中定义路由文件的路径来配置虚拟文件路由:

json
// tsr.config.json
{
  "virtualRouteConfig": "./routes.ts"
}
// tsr.config.json
{
  "virtualRouteConfig": "./routes.ts"
}

或者您可以在配置中直接定义虚拟路由,虽然这不太常见,但允许您通过在 tsr.config.json 文件中添加一个 virtualRouteConfig 对象,并定义您的虚拟路由,然后传递调用 @tanstack/virtual-file-routes 包中的实际 rootRoute/route/index/等函数生成的 JSON 来进行配置:

json
// tsr.config.json
{
  "virtualRouteConfig": {
    "type": "root",
    "file": "root.tsx",
    "children": [
      {
        "type": "index",
        "file": "home.tsx"
      },
      {
        "type": "route",
        "file": "posts/posts.tsx",
        "path": "/posts",
        "children": [
          {
            "type": "index",
            "file": "posts/posts-home.tsx"
          },
          {
            "type": "route",
            "file": "posts/posts-detail.tsx",
            "path": "$postId"
          }
        ]
      },
      {
        "type": "layout",
        "id": "first",
        "file": "layout/first-pathless-layout.tsx",
        "children": [
          {
            "type": "layout",
            "id": "second",
            "file": "layout/second-pathless-layout.tsx",
            "children": [
              {
                "type": "route",
                "file": "a.tsx",
                "path": "/route-a"
              },
              {
                "type": "route",
                "file": "b.tsx",
                "path": "/route-b"
              }
            ]
          }
        ]
      }
    ]
  }
}
// tsr.config.json
{
  "virtualRouteConfig": {
    "type": "root",
    "file": "root.tsx",
    "children": [
      {
        "type": "index",
        "file": "home.tsx"
      },
      {
        "type": "route",
        "file": "posts/posts.tsx",
        "path": "/posts",
        "children": [
          {
            "type": "index",
            "file": "posts/posts-home.tsx"
          },
          {
            "type": "route",
            "file": "posts/posts-detail.tsx",
            "path": "$postId"
          }
        ]
      },
      {
        "type": "layout",
        "id": "first",
        "file": "layout/first-pathless-layout.tsx",
        "children": [
          {
            "type": "layout",
            "id": "second",
            "file": "layout/second-pathless-layout.tsx",
            "children": [
              {
                "type": "route",
                "file": "a.tsx",
                "path": "/route-a"
              },
              {
                "type": "route",
                "file": "b.tsx",
                "path": "/route-b"
              }
            ]
          }
        ]
      }
    ]
  }
}
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
订阅 Bytes

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

Bytes

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

订阅 Bytes

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

Bytes

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