框架
版本

虚拟文件路由

我们要感谢 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 { TanStackRouterVite } from '@tanstack/router-plugin/vite'

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

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

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

tsx
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } 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: [TanStackRouterVite({ virtualRouteConfig: routes }), react()],
})
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } 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: [TanStackRouterVite({ 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
  ]),
])

物理虚拟路由

物理虚拟路由是一种在特定 URL 路径下“挂载”良好的旧式 TanStack Router 基于文件的路由约定的目录的方法。如果您正在使用虚拟路由来自定义路由树中较高层级的一小部分,但又想对子路由和目录使用标准的基于文件的路由约定,这将非常有用。

考虑以下文件结构

/routes
├── root.tsx
├── index.tsx
├── pathless.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
├── pathless.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

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

__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,并允许您通过默认导出定义子树配置。默认导出可以是:

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

Inception

您可以随意混合和匹配 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"
}

或者,您可以直接在配置中定义虚拟路由,虽然不太常见,但允许您通过将 virtualRouteConfig 对象添加到您的 tsr.config.json 文件并定义您的虚拟路由,并传递调用来自 @tanstack/virtual-file-routes 包的实际 rootRoute/route/index/etc 函数生成的 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"
              }
            ]
          }
        ]
      }
    ]
  }
}
订阅 Bytes

您的每周 JavaScript 新闻。每周一免费发送给超过 100,000 名开发者。

Bytes

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