为什么需要 Next.js 风格路由?
如果你的团队习惯了 Next.js App Router 的 app/ 目录、page.tsx 和 layout.tsx 命名约定,在迁移到 TanStack Start 时可能会感到不适应。TanStack Start 默认使用 routes/ 目录、index.tsx 和 route.tsx 的命名方式。
好消息是,TanStack Router 提供了灵活的配置选项,让你可以自定义文件命名约定,使其与 Next.js 保持一致!
默认对比
TanStack Start 默认风格
routes/
├── __root.tsx # 根布局
├── index.tsx # / 路由
├── about.tsx # /about 路由
└── blog/
├── route.tsx # /blog 布局
├── index.tsx # /blog 页面
└── $slug.tsx # /blog/:slug 动态路由
Next.js App Router 风格
app/
├── layout.tsx # 根布局
├── page.tsx # / 路由
├── about/
│ └── page.tsx # /about 路由
└── blog/
├── layout.tsx # /blog 布局
├── page.tsx # /blog 页面
└── [slug]/
└── page.tsx # /blog/:slug 动态路由
核心配置:让 TanStack Start 使用 Next.js 风格
关键在于 vite.config.ts 中的 TanStack Start 插件配置:
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
router: {
routesDirectory: './app', // ✅ 使用 app 目录代替 routes
indexToken: 'page', // ✅ 使用 page.tsx 代替 index.tsx
routeToken: 'layout', // ✅ 使用 layout.tsx 代替 route.tsx
routeFileIgnorePattern: // ✅ 忽略非路由文件
'^(?=.*\\.)((?!.*(?:^|\\/)(?:page|layout|__root)\\.(?:t|j)sx?$).)*$',
},
}),
],
})
配置项详解
| 配置项 | 默认值 | Next.js 风格值 | 说明 |
|---|---|---|---|
routesDirectory |
'./routes' |
'./app' |
路由文件所在目录 |
indexToken |
'index' |
'page' |
页面文件的命名 token |
routeToken |
'route' |
'layout' |
布局文件的命名 token |
routeFileIgnorePattern |
(默认) | (自定义正则) | 忽略非路由文件的模式 |
实际应用示例
配置完成后,你的项目结构就可以像 Next.js 一样组织了:
1. 根布局(app/__root.tsx)
根布局保持使用 __root.tsx 命名(这是 TanStack Router 的特殊约定):
// app/__root.tsx
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'TanStack Start App' },
],
}),
shellComponent: RootDocument,
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
2. 首页布局(app/layout.tsx)
现在可以使用 layout.tsx 代替原来的 route.tsx:
// app/layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: Layout,
})
function Layout() {
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 to-slate-800">
<Outlet />
</div>
)
}
3. 首页(app/page.tsx)
使用 page.tsx 代替原来的 index.tsx:
// app/page.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: HomePage,
})
function HomePage() {
return (
<div>
<h1>欢迎来到 TanStack Start</h1>
<p>使用 Next.js 风格的路由约定</p>
</div>
)
}
4. 嵌套路由示例
对于嵌套路由,文件结构如下:
app/
├── __root.tsx
├── layout.tsx # / 布局
├── page.tsx # / 页面
└── blog/
├── layout.tsx # /blog 布局
├── page.tsx # /blog 页面
└── $slug/
└── page.tsx # /blog/:slug 页面
博客布局:
// app/blog/layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/blog')({
component: BlogLayout,
})
function BlogLayout() {
return (
<div className="blog-container">
<nav className="blog-nav">
<h2>博客导航</h2>
</nav>
<main>
<Outlet /> {/* 渲染子路由 */}
</main>
</div>
)
}
博客列表页:
// app/blog/page.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blog/')({
component: BlogIndex,
})
function BlogIndex() {
return (
<div>
<h1>博客文章列表</h1>
</div>
)
}
动态路由(文章详情):
// app/blog/$slug/page.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blog/$slug')({
component: BlogPost,
})
function BlogPost() {
const { slug } = Route.useParams()
return (
<article>
<h1>文章: {slug}</h1>
<div>文章内容...</div>
</article>
)
}
关键差异:动态路由命名
需要注意的是,虽然我们可以使用 Next.js 风格的 page.tsx 和 layout.tsx,但动态路由的命名仍然保持 TanStack Router 的约定:
| 特性 | Next.js | TanStack Start (Next.js 风格配置后) |
|---|---|---|
| 页面文件 | page.tsx |
page.tsx ✅ |
| 布局文件 | layout.tsx |
layout.tsx ✅ |
| 动态参数 | [slug]/page.tsx |
$slug/page.tsx ⚠️ 仍使用 $ |
| Catch-all | [...slug]/page.tsx |
$.tsx ⚠️ 语法不同 |
| 可选 Catch-all | [[...slug]]/page.tsx |
(需自定义实现) |
示例目录结构对比:
# Next.js
app/
└── blog/
└── [slug]/
└── page.tsx
# TanStack Start (Next.js 风格)
app/
└── blog/
└── $slug/
└── page.tsx
完整的 Vite 配置示例
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import viteTsConfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [
viteTsConfigPaths({
projects: ['./tsconfig.json'],
}),
tanstackStart({
router: {
// 使用 app 目录
routesDirectory: './app',
// 使用 page.tsx 作为页面文件
indexToken: 'page',
// 使用 layout.tsx 作为布局文件
routeToken: 'layout',
// 忽略其他文件,只识别 page.tsx、layout.tsx 和 __root.tsx
routeFileIgnorePattern:
'^(?=.*\\.)((?!.*(?:^|\\/)(?:page|layout|__root)\\.(?:t|j)sx?$).)*$',
},
}),
viteReact(),
],
})
路由文件忽略模式详解
routeFileIgnorePattern 是一个正则表达式,用于指定哪些文件应该被忽略(不作为路由文件)。
上面的正则表达式的含义是:
- 忽略所有包含文件扩展名但不是
page.tsx/ts/jsx/js、layout.tsx/ts/jsx/js或__root.tsx/ts/jsx/js的文件
这样,你可以在 app/ 目录中放置其他辅助文件(如组件、工具函数等),它们不会被误认为是路由文件:
app/
├── __root.tsx
├── page.tsx
├── components/
│ └── Header.tsx # ✅ 会被忽略
├── utils/
│ └── helpers.ts # ✅ 会被忽略
└── blog/
├── layout.tsx
├── page.tsx
└── BlogCard.tsx # ✅ 会被忽略
优势与权衡
✅ 优势
- 降低迁移成本:团队从 Next.js 迁移时,保持熟悉的文件结构
- 提高可读性:
page.tsx和layout.tsx语义更明确 - 团队一致性:如果团队同时维护 Next.js 和 TanStack Start 项目,统一的命名减少认知负担
- 灵活配置:TanStack Router 允许你自定义任何命名约定
⚠️ 权衡
- 不是完全兼容:动态路由仍使用
$slug而不是[slug] - 学习曲线:需要理解配置项的含义
- 社区示例:大多数 TanStack Start 示例和文档使用默认约定,可能需要手动适配
何时使用 Next.js 风格路由?
推荐使用的场景:
- 团队正在从 Next.js 迁移到 TanStack Start
- 团队成员更熟悉 Next.js 的命名约定
- 同时维护 Next.js 和 TanStack Start 项目,希望保持一致性
不推荐使用的场景:
- 全新项目且团队没有 Next.js 背景
- 更喜欢 TanStack Router 的默认约定(
index.tsx、route.tsx更简洁) - 想要与 TanStack 社区示例保持一致
示例项目
一个完整的示例项目结构:
my-app/
├── app/
│ ├── __root.tsx
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/
│ │ └── page.tsx
│ ├── blog/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── $slug/
│ │ └── page.tsx
│ └── dashboard/
│ ├── layout.tsx
│ ├── page.tsx
│ └── settings/
│ └── page.tsx
├── src/
│ └── components/
│ └── Header.tsx
├── vite.config.ts
└── package.json
迁移步骤
如果你要从默认的 TanStack Start 项目迁移到 Next.js 风格:
步骤 1: 修改 Vite 配置
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
router: {
routesDirectory: './app',
indexToken: 'page',
routeToken: 'layout',
routeFileIgnorePattern:
'^(?=.*\\.)((?!.*(?:^|\\/)(?:page|layout|__root)\\.(?:t|j)sx?$).)*$',
},
}),
],
})
步骤 2: 重命名目录
# 将 routes 目录重命名为 app
mv routes app
步骤 3: 重命名文件
# 在 app 目录中
# index.tsx → page.tsx
# route.tsx → layout.tsx
# 示例
mv app/index.tsx app/page.tsx
mv app/blog/route.tsx app/blog/layout.tsx
mv app/blog/index.tsx app/blog/page.tsx
步骤 4: 重启开发服务器
pnpm dev
TanStack Router 会自动重新生成路由类型,现在你的项目就使用 Next.js 风格的路由了!
常见问题
Q1: 为什么 __root.tsx 不能改成 layout.tsx?
A: __root.tsx 是 TanStack Router 的特殊约定,用于定义应用的根路由和 HTML 结构。它不是普通的路由文件,因此保持这个命名。
Q2: 可以同时使用 index.tsx 和 page.tsx 吗?
A: 不建议。配置了 indexToken: 'page' 后,TanStack Router 会查找 page.tsx,忽略 index.tsx。为了保持一致性,应该统一使用一种命名方式。
Q3: 动态路由为什么还是用 $slug 而不是 [slug]?
A: 这是 TanStack Router 的核心约定,与 Vite 配置无关。$ 是 TanStack Router 识别动态参数的方式,无法通过配置改变。
Q4: 如何处理路由组(Route Groups)?
A: TanStack Router 使用 _ 前缀表示路由组(不会出现在 URL 中),这与 Next.js 的 (group) 概念类似:
# Next.js
app/(marketing)/about/page.tsx → /about
# TanStack Start
app/_marketing/about/page.tsx → /about
Q5: routeFileIgnorePattern 正则太复杂,可以简化吗?
A: 如果你只想识别 page.tsx 和 layout.tsx,可以使用更简单的模式:
routeFileIgnorePattern: '^(?!.*(?:page|layout|__root)\\.tsx?$).*$'
但上面的模式只匹配 .tsx 和 .ts 文件。原始的正则更全面,也支持 .jsx 和 .js。
总结
通过配置 vite.config.ts 中的 TanStack Start 插件选项,我们可以让 TanStack Start 使用与 Next.js App Router 相似的文件命名约定:
- ✅ 使用
app/目录代替routes/ - ✅ 使用
page.tsx代替index.tsx - ✅ 使用
layout.tsx代替route.tsx - ⚠️ 动态路由仍使用
$param而非[param]
这种配置对于从 Next.js 迁移的团队特别有用,降低了学习曲线,同时保留了 TanStack Router 强大的类型安全和灵活性。