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 跨层级跳转

注意:在 LinkuseNavigate 中,路径规则是一样的。我曾经在 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 反而可能导致子组件不更新。

嵌套路由这个东西,说白了就是「分而治之」。把大页面拆成小布局,每个布局只关心自己的事。等你用习惯了,就会发现它真的很香。