4、嵌套路由:从页面到布局的进阶之路
说实话,刚接触 React Router 的时候,我总觉得路由就是「URL 对应组件」这么简单。直到我在一个后台管理系统里,遇到了左侧菜单栏 + 右侧内容区的经典布局——每个菜单项切换时,左侧不动,右侧换内容。这时候我才意识到,嵌套路由才是真正解决这类问题的利器。
嵌套路由的概念
嵌套路由,说白了就是「路由里面套路由」。你想想看,一个页面往往由多个区域组成:顶部导航、侧边栏、主内容区。如果每次切换页面都要重新渲染整个布局,那体验就太差了。
嵌套路由的核心思想是:父路由负责布局框架,子路由负责填充内容。父组件渲染后,子组件在父组件内部的某个位置「插进去」。
关键点:嵌套路由不是简单的 URL 拼接,而是组件树的层级嵌套。每个层级的路由都对应一个组件,子路由的组件会被渲染到父路由组件的 <Outlet /> 位置。
Outlet 组件:子路由的「插座」
Outlet 是 React Router v6 提供的一个内置组件。它的作用很简单——占位。父组件里放一个 <Outlet />,子路由匹配到的组件就会渲染到这个位置。
我记得第一次用的时候,还纳闷为什么子组件不显示。后来才发现,原来父组件里忘了加 Outlet。嗯,这个坑我踩过,你们别踩了。
// 父布局组件
import { Outlet } from 'react-router-dom';
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<!-- 侧边栏菜单 -->
<nav>...</nav>
</aside>
<main>
<Outlet /> <!-- 子路由内容会渲染到这里 -->
</main>
</div>
);
}
对应的路由配置是这样的:
const router = createBrowserRouter([
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'settings', element: <Settings /> },
{ path: 'users', element: <Users /> },
],
},
]);
当用户访问 /dashboard/settings 时,DashboardLayout 渲染,然后 Settings 组件出现在 <Outlet /> 的位置。布局框架保持不变,只有内容区域更新。
小技巧:Outlet 可以嵌套多层。比如你可以在 DashboardLayout 里再嵌套一个 TabLayout,实现「布局套布局」的效果。我做过一个项目,嵌套了 4 层,依然很流畅。
相对路径与绝对路径
这是新手最容易搞混的地方。我刚开始也经常写错路径,导致路由匹配不上。
绝对路径:以 / 开头,从根路径开始匹配。
// 绝对路径
{ path: '/dashboard/settings', element: <Settings /> }
相对路径:不以 / 开头,相对于父路由的路径。
// 相对路径(在父路由 children 中)
{ path: 'settings', element: <Settings /> }
// 实际匹配:/dashboard/settings
我个人习惯在嵌套路由里全部使用相对路径。为什么呢?因为这样路由配置更清晰,父路由改了路径,子路由不用跟着改。你想想看,如果父路由从 /dashboard 改成 /admin,绝对路径的子路由全都要改一遍,多麻烦。
| 路径类型 | 写法 | 实际匹配 | 适用场景 |
|---|---|---|---|
| 绝对路径 | /dashboard/settings |
/dashboard/settings |
独立页面、顶层路由 |
| 相对路径 | settings |
父路径 + /settings |
嵌套子路由 |
| 相对路径(上级) | ../settings |
父路径的父路径 + /settings |
跨层级跳转 |
注意:在 Link 和 useNavigate 中,路径规则是一样的。我曾经在 Link 里写了个 to="settings",结果它拼接了当前 URL 而不是父路由的 URL,排查了半天才发现是路径写错了。
布局路由设计
布局路由,说白了就是用嵌套路由来实现页面布局的复用。这是我在实际项目中最常用的模式。
一个典型的后台管理系统,布局结构通常是这样的:
// 路由配置
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />, // 最外层布局(顶部导航 + 底部)
children: [
{ index: true, element: <Home /> },
{
path: 'dashboard',
element: <DashboardLayout />, // 侧边栏 + 内容区
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'analytics', element: <Analytics /> },
{ path: 'reports', element: <Reports /> },
],
},
{
path: 'auth',
element: <AuthLayout />, // 登录/注册布局(居中卡片)
children: [
{ path: 'login', element: <Login /> },
{ path: 'register', element: <Register /> },
],
},
],
},
]);
这种设计的好处很明显:
- 代码复用:布局组件只写一次,所有子页面共享
- 状态保持:父布局的状态(比如侧边栏展开/收起)不会因为子路由切换而丢失
- 权限控制:可以在父布局里统一做权限校验,子路由不用重复写
- 加载优化:父布局只需要加载一次,子路由按需加载
我的经验:布局路由设计时,建议把「公共部分」尽量往上层放。比如顶部导航放在 RootLayout,侧边栏放在 DashboardLayout。这样每个层级的职责清晰,后期维护也方便。
我曾经接手过一个项目,所有布局逻辑都写在一个组件里,一个文件 2000 多行。后来我重构成了嵌套路由 + 布局组件,每个文件控制在 100 行以内。嗯,改完之后,团队的小伙伴都说「终于能看懂了」。
避坑指南
最后分享几个我踩过的坑:
- Outlet 必须存在:父组件里没有
<Outlet />,子路由永远不会渲染。这是最常见的错误。 - 路径不要乱加斜杠:相对路径不要以
/开头,否则会变成绝对路径。 - index 路由:用
index: true表示默认子路由,不需要写path。 - 布局组件不要用
React.memo:因为 Outlet 的内容变化时,父组件需要重新渲染。用了 memo 反而可能导致子组件不更新。
嵌套路由这个东西,说白了就是「分而治之」。把大页面拆成小布局,每个布局只关心自己的事。等你用习惯了,就会发现它真的很香。