TanStack Router 实战:如何构建经典的“左侧菜单 + 右侧内容”后台布局
在开发企业级后台管理系统(Admin Dashboard)时, “左侧固定菜单 + 右侧动态内容” 是最经典的布局模式。同时,我们通常还需要一个独立的登录页面,它不包含菜单栏,而是全屏显示。
在使用 TanStack Router 这种基于文件系统的路由库时,如何优雅地实现这两种截然不同的布局共存,且保持 URL 简洁(例如访问 /users 而不是 /admin/users)?
答案就是使用 无路径布局路由 (Pathless Layout Route) 。本文将带你一步步落地这个架构。
1. 核心概念:什么是无路径布局?
在 TanStack Router 中,如果你希望创建一个“包裹器”组件(比如包含侧边栏的 Layout),但不希望它在 URL 中增加一层路径,你需要在文件名加一个下划线前缀 _。
-
routes/admin.tsx: 会生成/admin/...的 URL 路径。 -
routes/_layout.tsx: 不会生成 URL 路径,它只是一个逻辑上的包裹层。
利用这个特性,我们可以实现:
-
/login-> 渲染独立的登录页。 -
/-> 渲染_layout(带菜单) -> 渲染index(仪表盘)。 -
/users-> 渲染_layout(带菜单) -> 渲染users(用户列表)。
2. 推荐的文件结构
这是实现该架构的最佳目录结构。请注意 _layout 文件夹的使用:
src/
└── routes/
├── __root.tsx # 根组件 (通常只放 Context/DevTools)
├── login.tsx # 独立的登录页 (无菜单)
├── _layout.tsx # ✨ 核心:后台布局主文件
└── _layout/ # ✨ 核心:布局内部的子页面目录
├── index.tsx # 对应 URL: / (仪表盘)
├── users.tsx # 对应 URL: /users
└── settings.tsx # 对应 URL: /settings
⚠️ 必须注意:如果你创建了
src/routes/_layout/index.tsx,请务必删除项目根目录下的src/routes/index.tsx,否则会报“路由冲突”错误。
3. 代码实现
第一步:编写布局容器 (src/routes/_layout.tsx)
这是整个架构的骨架。我们需要在这里划分左右区域,并放置 <Outlet />。
import { createFileRoute, Outlet, Link } from '@tanstack/react-router'
export const Route = createFileRoute('/_layout')({
component: AdminLayout,
})
function AdminLayout() {
return (
<div className="flex h-screen w-full bg-gray-100">
{/* --- 左侧侧边栏 --- */}
<aside className="w-64 bg-gray-900 text-white flex flex-col shadow-lg">
<div className="p-6 text-xl font-bold border-b border-gray-800">
Admin System
</div>
<nav className="flex-1 p-4 space-y-2">
<MenuLink to="/" label="仪表盘" />
<MenuLink to="/users" label="用户管理" />
<MenuLink to="/settings" label="系统设置" />
</nav>
</aside>
{/* --- 右侧内容区域 --- */}
<main className="flex-1 flex flex-col overflow-hidden">
{/* 顶部通栏 (Header) */}
<header className="h-16 bg-white shadow-sm flex items-center px-6">
<span className="text-gray-500">面包屑 / 顶部导航</span>
</header>
{/* 页面内容滚动区 */}
<div className="flex-1 overflow-auto p-6">
{/* ✨✨✨ 关键点:子路由渲染出口 ✨✨✨ */}
<Outlet />
</div>
</main>
</div>
)
}
// 封装一个简单的菜单组件,自动处理高亮
function MenuLink({ to, label }: { to: string; label: string }) {
return (
<Link
to={to}
className="block px-4 py-2 rounded transition-colors hover:bg-gray-800"
// 激活时的样式
activeProps={{ className: 'bg-blue-600 text-white shadow' }}
// 首页路由需要精确匹配,防止所有页面都高亮它
activeOptions={{ exact: to === '/' }}
>
{label}
</Link>
)
}
第二步:编写子页面
子页面文件放在 src/routes/_layout/ 目录下。
仪表盘 (src/routes/_layout/index.tsx):
import { createFileRoute } from '@tanstack/react-router'
// 注意:参数必须匹配文件路径
export const Route = createFileRoute('/_layout/')({
component: () => (
<div className="bg-white p-8 rounded shadow">
<h1 className="text-2xl font-bold mb-4">欢迎回来</h1>
<p>这里是仪表盘的核心数据区域。</p>
</div>
),
})
用户管理 (src/routes/_layout/users.tsx):
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_layout/users')({
component: () => <div>用户列表管理界面</div>,
})
第三步:独立的登录页 (src/routes/login.tsx)
因为它不在 _layout 文件夹内,所以它不会继承左侧菜单。
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/login')({
component: () => (
<div className="h-screen w-full flex items-center justify-center bg-gray-200">
<div className="p-10 bg-white rounded-lg shadow-xl">
<h1 className="text-2xl font-bold">请登录</h1>
{/* 登录表单... */}
</div>
</div>
),
})
4. 常见坑点与排查 (Troubleshooting)
在搭建这套架构时,90% 的开发者会遇到以下三个问题:
坑点一:路由冲突 (Conflicting configuration paths)
现象:终端报错 Conflicting configuration paths were found for the following routes: "/", "/"。
原因:你的旧文件 src/routes/index.tsx 和新文件 src/routes/_layout/index.tsx 都试图代表根路径 /。
解决:直接删除 src/routes/index.tsx。
坑点二:右侧一片空白
现象:能看到左侧菜单,URL 也没错,但右边内容区是空的。
原因:你忘记在 _layout.tsx 里写 了。
解决:在
标签内部添加 组件。坑点三:TS 爆红波浪线
现象:刚创建 _layout.tsx 时,createFileRoute('/_layout') 处提示类型错误。
原因:TanStack Router 还没来得及生成类型定义。
解决:
- 保存文件(即使有错)。
- 等待终端显示生成完成。
- 如果还没好,按
Cmd/Ctrl + Shift + P->TypeScript: Restart TS Server。
5. 总结
通过使用 _layout.tsx (无路径布局),我们成功实现了:
- 结构清晰:后台页面集中管理,与登录页物理隔离。
-
URL 简洁:用户访问的是
/users而非繁琐的/layout/users。 -
开发高效:配合
<Outlet />和自动高亮的<Link />,几分钟就能搭好骨架。
这套方案是目前 TanStack Router 构建中后台系统的最佳实践。