译:TanStack 的虚拟文件路由

原文:https://tanstack.com/router/v1/docs/framework/react/guide/virtual-file-routes
译者:ChatGPT 4 Turbo

编者注:这是把 Remix Router 未发布的功能直接实现了啊。虚拟文件路由是提供了编程式的接口,同时和编译时插件结合,编码声明时不用把文件路径写全,写相对路径就够。

我们要感谢 Remix 团队开创虚拟文件路由的概念。我们从他们的工作中获得了灵感,并将其适配到 TanStack Router 现有的基于文件的路由树生成中。

虚拟文件路由是一个强大的概念,它允许你使用引用项目中真实文件的代码,以编程方式构建一个路由树。如果你有以下需求,这会非常有用:

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

这里有一个快速示例,展示如何使用虚拟文件路由将一个路由树映射到项目中的一组真实文件:

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

const virtualRouteConfig = rootRoute('root.tsx', [
  index('index.tsx'),
  layout('layout.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'),
  ]),
])
import {
  rootRoute,
  route,
  index,
  layout,
  physical,
} from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
  index('index.tsx'),
  layout('layout.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'),
  ]),
])

配置

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

  • TanStackRouter 插件,适用于 Vite/Rspack/Webpack
  • TanStack 路由器 CLI 的 tsr.config.json 文件

通过 TanStackRouter 插件进行配置

如果你正在使用 TanStackRouter 插件用于 Vite/Rspack/Webpack,你可以通过向插件传递 virtualRoutesConfig 选项来配置虚拟文件路由:

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', [
  // ... 你的虚拟路由树的其余部分
])

export default defineConfig({
  plugins: [TanStackRouterVite({ virtualRouteConfig: routes }), react()],
})
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', [
  // ... 你的虚拟路由树的其余部分
])

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

创建虚拟文件路由

要创建虚拟文件路由,你需要导入 @tanstack/virtual-file-routes 包。此包提供了一套函数,允许你创建引用项目中实际文件的虚拟路由。从包中导出了一些实用函数:

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

虚拟根路由

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

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

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

const virtualRouteConfig = rootRoute('root.tsx', [
  // ... 子路由
])

虚拟路由

路由函数用于创建一个虚拟路由。它需要一个路径、一个文件名和一个子路由数组。下面是一个虚拟路由的例子:

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

const virtualRouteConfig = rootRoute('root.tsx', [
  route('/about', 'about.tsx', [
    // ... 子路由
  ]),
])
import { route } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
  route('/about', 'about.tsx', [
    // ... 子路由
  ]),
])

虚拟索引路由

index 函数用于创建一个虚拟索引路由。它需要一个文件名。下面是一个虚拟索引路由的例子:

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

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

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

虚拟布局路由

layout 函数用于创建一个虚拟布局路由。它需要一个文件名、一个子路由数组和一个可选的布局 ID。下面是一个虚拟布局路由的例子:

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

const virtualRouteConfig = rootRoute('root.tsx', [
  layout('layout.tsx', [
    // ... 子路由
  ]),
])
import { layout } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
  layout('layout.tsx', [
    // ... 子路由
  ]),
])

你也可以指定一个布局 ID,为布局提供一个与文件名不同的唯一标识符:

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

const virtualRouteConfig = rootRoute('root.tsx', [
  layout('my-layout-id', 'layout.tsx', [
    // ... 子路由
  ]),
])
import { layout } from '@tanstack/virtual-file-routes'

const virtualRouteConfig = rootRoute('root.tsx', [
  layout('my-layout-id', 'layout.tsx', [
    // ... 子路由
  ]),
])

物理虚拟路径

物理虚拟路径是一种将好老的 TanStack 路由文件基础路由惯例的目录 “挂载” 在特定的 URL 路径下的方式。如果您在层次结构较高的部分使用虚拟路由来自定义路由树的一小部分,但希望对子路由和目录使用标准的基于文件的路由惯例,这可能很有用。

考虑以下文件结构:

/routes
├── root.tsx
├── index.tsx
├── layout.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
├── layout.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 路径下:

const virtualRouteConfig = rootRoute('root.tsx', [
  // 按常规设置您的虚拟路由
  index('index.tsx'),
  layout('layout.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'),
      ]),
    ]),
    // 将 `posts` 目录挂载在 `/posts` 路径下
    physical('/posts', 'posts'),
  ]),
])
const virtualRouteConfig = rootRoute('root.tsx', [
  // 按常规设置您的虚拟路由
  index('index.tsx'),
  layout('layout.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'),
      ]),
    ]),
    // 将 `posts` 目录挂载在 `/posts` 路径下
    physical('/posts', 'posts'),
  ]),
])

在 TanStack 路由器文件基础路由中的虚拟路由

上一节向您展示了如何在虚拟路由配置中使用 TanStack 路由器的文件基础路由约定。然而,反之亦然。
您可以使用 TanStack 路由器的文件基础路由约定配置应用的主要路由树部分,并为特定子树选择虚拟路由配置。

考虑以下文件结构:

/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:

import {
  defineVirtualSubtreeConfig,
  index,
  route,
} from '@tanstack/virtual-file-routes'

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

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

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

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

嵌套

您可以根据喜好混合匹配 TanStack 路由器的文件基础路由约定和虚拟路由配置。

让我们深入了解!
查看以下示例,该示例从使用基于文件的路由约定开始,转为 /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 配置

虽然不太常见,但您也可以通过在 tsr.config.json 文件中添加 virtualRouteConfig 对象并定义您的虚拟路由,然后调用 @tanstack/virtual-file-routes 包的 actual rootRoute/route/index 等函数生成的 JSON 来通过 TanStack Router CLI 配置虚拟文件路由:

// 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-layout.tsx",
        "children": [
          {
            "type": "layout",
            "id": "second",
            "file": "layout/second-layout.tsx",
            "children": [
              {
                "type": "route",
                "file": "a.tsx",
                "path": "/layout-a"
              },
              {
                "type": "route",
                "file": "b.tsx",
                "path": "/layout-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-layout.tsx",
        "children": [
          {
            "type": "layout",
            "id": "second",
            "file": "layout/second-layout.tsx",
            "children": [
              {
                "type": "route",
                "file": "a.tsx",
                "path": "/layout-a"
              },
              {
                "type": "route",
                "file": "b.tsx",
                "path": "/layout-b"
              }
            ]
          }
        ]
      }
    ]
  }
}