文件式路由
本教程共 38 篇 · 第 16 篇 · 更新于 2026-07-27 · 约 13 分钟阅读
16. 文件式路由
本节目标:学会用文件命名约定让 TanStack Router 自动生成路由树。搞懂
__root、点分隔、$动态参数、_无路径布局等规则,配好 Vite 插件,让routeTree.gen.ts自动更新。学完你能用文件式路由搭出一个多层嵌套的应用。
16.1 文件式路由是啥
上一章我们用代码式路由,手写 createRoute、addChildren,一步步拼路由树。文件多了之后这活又累又容易出错—加个页面要改好几处。
文件式路由换了个思路:路由结构直接由文件结构决定。你在 routes/ 目录下建文件、起名字,构建工具自动扫这些文件,生成路由树代码。加页面就建个文件,删页面就删文件,路由树自动跟着变。
它的好处官方列了一串:
- 直观:文件结构就是 URL 结构,一眼看明白。
- 好维护:加路由不用改一堆配置文件。
- 自动代码分割:每个路由文件默认懒加载,首屏更快。
- 类型安全更强:生成的代码里带类型链路,比手写更准。
- 一致性强:项目间结构统一,换个项目也熟门熟路。
Note文件式路由是官方推荐方式,功能上和代码式完全等价。但代码更省、类型更稳。除非有特殊需求,新项目都用文件式。
16.2 两种写法:点分隔 vs 目录
文件式路由里表达”嵌套”有两种写法,能混着用。
16.2.1 点分隔(Flat Routes)
文件名里用 . 表示路由嵌套层级。比如 posts.$postId.tsx 表示 /posts/$postId,posts 是父,$postId 是子。不用建一堆目录,一个文件名搞定多层。
适合”层数多但每层文件少”的场景,省得为了一两个文件建一堆文件夹。
16.2.2 目录(Directory Routes)
用目录表示嵌套。posts/ 目录下的文件都是 posts 的子路由。
posts/
├── index.tsx # /posts
├── $postId.tsx # /posts/$postId
适合”同一层级文件多”的场景,放目录里更整齐。
16.2.3 混合用
实际项目里两种混着用最常见。同一项目里有的地方用点分隔,有的地方用目录,按需选:
routes/
├── __root.tsx
├── index.tsx # /
├── posts/ # 用目录
│ ├── index.tsx # /posts
│ ├── $postId.tsx # /posts/$postId
│ ├── $postId.edit.tsx # /posts/$postId/edit
├── settings.profile.tsx # 用点分隔
├── settings.notifications.tsx
posts/ 下面文件多用目录,settings 下面就两个文件用点分隔。怎么顺手怎么来。
16.3 命名约定全表
文件式路由靠文件名编码路由信息,规则不多但要记牢。这张表是核心:
| 文件名特征 | 含义 | 例子 |
|---|---|---|
__root.tsx | 根路由文件,必须放 routesDirectory 根目录 | __root.tsx |
. 分隔符 | 表示路由嵌套 | blog.post.tsx 是 blog 的子路由 |
$ 标记 | 动态路径参数,从 URL 提取值 | posts.$postId.tsx 匹配 /posts/123 |
_ 前缀 | 无路径布局路由,不占 URL 段 | _app.tsx 包住子路由但 URL 里没 _app |
_ 后缀 | 脱离父级嵌套,不共享父布局 | posts_.$postId.edit.tsx 不套 <Posts> |
- 前缀 | 排除文件/目录,不进路由树 | -components/header.tsx 被忽略 |
(folder) | 路由分组目录,不影响 URL | (auth)/login.tsx URL 是 /login |
[x] 转义 | 转义特殊字符 | script[.]js.tsx 变成 /script.js |
index 标记 | 索引路由,匹配父路径精确访问 | posts.index.tsx 匹配 /posts |
.route.tsx | 目录里的路由文件 | posts/route.tsx 是 /posts 的布局 |
Tip这张表是后面所有文件式路由操作的基础。建议收藏,写代码时对照看。命名错了路由就匹配不上,调试半天才发现是文件名拼错。
16.4 各种路由文件怎么写
约定清楚了,看每种路由文件具体长啥样。文件式路由用 createFileRoute 函数创建路由,参数是文件路径(这个路径由插件自动生成和管理,你不用手写)。
16.4.1 根路由 __root.tsx
根路由必须叫 __root.tsx,放在 routes/ 根目录。它包住所有路由,组件永远渲染。
import { createRootRoute, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<div>
<h1>我的应用</h1>
<Outlet />
</div>
),
})
createRootRoute 不接收路径参数。组件里放 <Outlet /> 给子路由留位置。
16.4.2 基础路由
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>关于我们</div>
}
createFileRoute('/about') 里的 /about 是插件根据文件路径自动生成的,你不用自己写。但必须传,因为 TypeScript 要靠它知道当前在哪个路由文件里。
16.4.3 索引路由
文件名以 index 结尾(在扩展名前),匹配父路由的精确访问。
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
function PostsIndexComponent() {
return <div>请选择一篇文章</div>
}
注意路径是 /posts/,结尾有个斜杠,表示索引。
16.4.4 动态路由
$ 开头的段是动态参数,能从 URL 提取值。
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// loader 里能拿到 params
loader: ({ params }) => fetchPost(params.postId),
// 组件里也能拿
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
return <div>文章 ID:{postId}</div>
}
访问 /posts/123 时,params.postId 就是 '123'。$ 能出现在任意段,/posts/$postId/$revisionId 也行,每个 $ 段都进 params。
16.4.5 通配路由
文件名只是 $.tsx 的路由,匹配从 $ 到结尾的所有内容。
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/files/$')({
component: FilesComponent,
})
function FilesComponent() {
const params = Route.useParams()
// params._splat 是 'documents/hello-world' 这种
return <div>路径:{params._splat}</div>
}
访问 /files/documents/hello-world,params._splat 是 'documents/hello-world'。常用作 404 兜底。
Warningv1 里通配参数 key 是
_splat,同时兼容旧的*,v2 会移除*。新代码用_splat。
16.4.6 布局路由
文件名是 app.tsx,子路由用 app.xxx.tsx 命名,app.tsx 就成了布局。
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/app')({
component: AppLayoutComponent,
})
function AppLayoutComponent() {
return (
<div>
<h1>App 布局</h1>
<Outlet />
</div>
)
}
routes/
├── app.tsx # 布局
├── app.dashboard.tsx # /app/dashboard,套 <AppLayout>
├── app.settings.tsx # /app/settings,套 <AppLayout>
| URL | 渲染 |
|---|---|
/app | <AppLayout> |
/app/dashboard | <AppLayout><Dashboard> |
/app/settings | <AppLayout><Settings> |
用目录也行,app/route.tsx 当布局,app/dashboard.tsx、app/settings.tsx 当子路由。
16.4.7 无路径布局路由
_ 前缀的路由,有布局功能但不占 URL 段。
routes/
├── _pathlessLayout.tsx # 布局,但 URL 里没这层
├── _pathlessLayout.route-a.tsx # /route-a
├── _pathlessLayout.route-b.tsx # /route-b
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_pathlessLayout')({
component: PathlessLayoutComponent,
})
function PathlessLayoutComponent() {
return (
<div>
<h1>无路径布局</h1>
<Outlet />
</div>
)
}
| URL | 渲染 |
|---|---|
/route-a | <PathlessLayout><A> |
/route-b | <PathlessLayout><B> |
URL 里不出现 _pathlessLayout,但组件套了一层布局。适合”一组页面共享布局但 URL 不想体现分组”的场景。
Warning无路径布局路由不支持动态段。
_$postId/这种写法不行,因为_前缀的路径不参与 URL 匹配,$postId没法从 URL 提取。要动态段就别用_前缀。
16.4.8 非嵌套路由
_ 后缀(注意是后缀不是前缀)让路由脱离父级嵌套。
routes/
├── posts.tsx # /posts
├── posts.$postId.tsx # /posts/123,套 <Posts>
├── posts_.$postId.edit.tsx # /posts/123/edit,不套 <Posts>
posts_.$postId.edit.tsx 名字里有 posts,但因为 posts_ 后缀,渲染时不套 <Posts> 组件,直接渲染 <PostEditor>。适合”URL 上属于某组但布局上独立”的场景。
16.4.9 排除文件
- 前缀的文件和目录不进路由树,用来放路由相关的辅助代码(组件、工具函数等)。
routes/
├── posts.tsx
├── -posts-table.tsx # 忽略,不进路由树
├── -components/ # 整个目录忽略
│ ├── header.tsx
│ ├── footer.tsx
在 posts.tsx 里能正常 import 这些文件:
import { createFileRoute } from '@tanstack/react-router'
import { PostsTable } from './-posts-table'
import { PostsHeader } from './-components/header'
export const Route = createFileRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
return (
<div>
<PostsHeader />
<PostsTable />
</div>
)
}
这样能把路由相关的组件和路由文件放一起(colocate),又不影响路由树。
16.4.10 路由分组目录
(folder) 形式的目录是纯组织用的,不影响 URL 和路由树。
routes/
├── index.tsx
├── (app)/
│ ├── dashboard.tsx
│ ├── settings.tsx
├── (auth)/
│ ├── login.tsx
│ ├── register.tsx
| URL | 渲染 |
|---|---|
/ | <Index> |
/dashboard | <Dashboard> |
/login | <Login> |
(app) 和 (auth) 在 URL 里不出现,纯粹是把相关路由文件归到一起方便管理。和 - 前缀的区别是:- 前缀的文件不进路由树,(folder) 里的文件进路由树但目录名不进 URL。
16.5 配置 Vite 插件
文件式路由要靠构建工具扫文件、生成路由树。Vite 项目装 @tanstack/router-plugin。
16.5.1 安装插件
npm install -D @tanstack/router-plugin
16.5.2 配置 vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
// 注意:router-plugin 必须在 plugin-react 之前
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
}),
react(),
],
})
Warning
tanstackRouter插件必须放在react()之前,顺序错了会出问题。这是新手常踩的坑,配置时务必注意。
16.5.3 默认配置
插件有合理的默认值,多数项目不用改:
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/routeTree.gen.ts",
"routeFileIgnorePrefix": "-",
"quoteStyle": "single"
}
- 路由文件目录:
./src/routes - 生成的路由树文件:
./src/routeTree.gen.ts - 忽略前缀:
-(这个前缀的文件不进路由树) - 引号风格:单引号
要改就在 tanstackRouter({...}) 里传参覆盖。
16.6 routeTree.gen.ts 是啥
配好插件后,启动 dev server 或 build 时,插件会扫 src/routes/ 下的所有文件,自动生成 src/routeTree.gen.ts。这个文件就是路由树的代码表示。
它的开头通常长这样:
/* eslint-disable */
// @ts-nocheck
// 这个文件由 TanStack Router 自动生成
// 不要手动修改,会被覆盖
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as PostsRouteImport } from './routes/posts'
// ...
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
// ...
然后是一大堆类型定义:
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/posts': typeof PostsRoute
// ...
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/posts': typeof PostsRoute
// ...
}
这些类型是 TanStack Router 类型安全的基石。<Link to="/posts"> 之所以能检查路由是否存在,就是靠这些自动生成的类型。
Note这个文件千万别手动改,每次构建都会被覆盖。也建议在 linter/formatter 里忽略它,避免格式化工具改了它导致冲突。
16.7 用生成的路由树创建 Router
有了 routeTree.gen.ts,创建 Router 就很简单:
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
defaultPreload: 'intent',
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
export default router
routeTree 直接从生成的文件 import 进来。declare module 那段别忘了,类型安全靠它。
然后在入口文件挂载:
import { RouterProvider } from '@tanstack/react-router'
import ReactDOM from 'react-dom/client'
import router from './router'
ReactDOM.createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />,
)
16.8 日常开发流程
文件式路由的日常开发特别省心:
- 加页面:在
src/routes/下建个文件,比如contact.tsx,写好createFileRoute('/contact')({...})。保存后插件自动重新生成routeTree.gen.ts,新路由立即可用。 - 删页面:删掉文件,路由树自动更新。
- 改路由结构:改文件名就行。
about.tsx改成info.about.tsx,路由从/about变成/info/about。 - 加嵌套:建
posts/$postId.tsx,自动成为posts.tsx的子路由。
整个过程不用手动维护路由树代码,文件结构就是路由结构。
TipVSCode 用户建议把
routeTree.gen.ts标记为只读,避免误改。在.vscode/settings.json里加:
{
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
}
}
这样改路由文件后 VSCode 不会突然弹出 routeTree.gen.ts 报错,体验更顺。
16.9 常见坑
坑一:插件顺序错了。 tanstackRouter 必须在 react() 之前,放后面路由生成会出问题。配置文件里检查顺序。
坑二:手动改 routeTree.gen.ts。 改了也被下次构建覆盖。要改路由就改路由文件本身。
坑三:createFileRoute 路径写错。 这个路径是插件生成的,但你要复制对。写成 /about 还是 /about/ 区别很大,结尾斜杠表示索引路由。
坑四:_ 前缀和后缀搞混。 _ 在前是无路径布局,_ 在后是脱离父级嵌套。两者完全不同,名字相似最容易混。
坑五:动态段和无路径布局混用。 _$postId/ 这种写法不行,_ 前缀的路径不参与 URL 匹配,动态段没值可取。要动态段就别用 _ 前缀。
坑六:忘了 declare module。 不注册 router 类型,<Link to="..."> 的类型检查就失效。每次创建 router 后都加上。
坑七:- 前缀和 (folder) 分不清。 - 前缀的文件不进路由树,(folder) 里的文件进路由树但目录名不进 URL。一个完全不参与路由,一个参与但不影响 URL。
16.10 小结
文件式路由的要点捋一遍:
- 用文件结构表达路由结构,构建工具自动生成路由树代码。
- 嵌套用
.分隔或目录,两种能混用。 - 命名约定:
__root是根,$是动态参数,_前缀是无路径布局,_后缀是脱离父级,-前缀是排除,(folder)是分组。 - Vite 项目装
@tanstack/router-plugin,放在react()之前。 routeTree.gen.ts自动生成、别手改、注册类型靠declare module。- 日常加删页面就是加删文件,路由树自动同步。
文件式路由是 TanStack Router 的推荐玩法,省心省力。下一章讲代码式路由,看什么场景下需要手写路由树,以及怎么用虚拟文件路由把两者结合起来。