我们要感谢 Remix 团队率先提出了虚拟文件路由的概念。我们从他们的工作中汲取了灵感,并对其进行了调整,使其与 TanStack Router 现有的基于文件的路由树生成一起工作。
虚拟文件路由是一个强大的概念,它允许您使用引用项目中真实文件的代码以编程方式构建路由树。这在以下情况下很有用:
这是一个使用虚拟文件路由将路由树映射到项目中一组真实文件的快速示例
// 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 插件,您可以通过将路由文件的路径传递给设置插件时的 virtualRoutesConfig 选项来配置虚拟文件路由
// 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(),
],
})
或者,您可以选择直接在配置中定义虚拟路由
// 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 函数用于创建虚拟根路由。它接受文件名和子路由数组。这是一个虚拟根路由的示例:
// 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 函数用于创建虚拟路由。它接受路径、文件名和子路由数组。这是一个虚拟路由的示例:
// 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
]),
])
您还可以定义没有文件名的虚拟路由。这允许为其子项设置公共路径前缀
// 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 函数用于创建虚拟索引路由。它接受文件名。这是一个虚拟索引路由的示例:
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。这是一个虚拟无路径路由的示例:
// 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,以赋予路由与文件名不同的唯一标识符
// 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 路径下
// 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 的基于文件的路由约定配置应用程序的路由树的主要部分,并选择为特定的子树使用虚拟路由配置。
考虑以下文件结构
/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
// 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,您可以通过在 tsr.config.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 来配置它们
// 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"
}
]
}
]
}
]
}
}
您的每周 JavaScript 新闻。每周一免费发送给超过 100,000 名开发者。