系统设计 / 路由设计 · 稳定入口与可变目标 / 嵌套路由与 layout 待审核 16 / 23

嵌套路由与 layout

匹配引擎里,一条 path 命中一条路由、得到一个 handler。真实前端(React Router、Vue Router、Next.js)的路由是一棵树:URL 的每一段对应树里的一层,一条 path 命中的是从根到叶的一整条。链上每个节点都可以带一个 layout,它们层层嵌套地包住最终的页面——外层画框架(顶栏、侧边栏),内层在它留出的 outlet 槽位里渲染下一层。

这带来一个平表给不了的性质:切换兄弟页面时,共享的外层 layout 原地不动。从 /dashboard/settings/profile 跳到 /dashboard/settings/billing,顶栏、侧边栏与设置页的标签条全部保持挂载,滚动位置、展开状态与输入都不丢,只有最内层那一格被换掉。

图 0-1 · 不同 path 命中的链与 layout 的嵌套关系,每层标出本次导航是复用还是新挂。可在 /dashboard/settings/profile/dashboard/settings/billing 之间来回点,观察前三层始终标为复用。

复用是嵌套路由最值钱的性质。在 profilebilling 之间切换时,命中链的前三层(AppShellDashboardLayoutSettingsLayout)完全相同,框架不卸载也不重建它们,侧边栏的展开状态与标签条位置都原样保留,只有最内层被换下。

1 · index 路由

/dashboard 命中了 DashboardLayout,但 layout 只是个带 outlet 的框,它那一格该显示什么?答案是 index 路由:一条没有自己 path 段、专门填充父 layout 默认内容的子路由,对应 React Router 的 index 与 Vue Router 的默认子路由。所以 /dashboard 实际命中链是 AppShellDashboardLayoutOverview,三层,最后那层是 index。

嵌套匹配把一条 path 走成一条根到叶的链:段段下行,缺段则取 index。

// 路由是一棵树; 一条 path 命中的是「根 → 叶」的一整条链。
function matchChain(node, segs) {
  if (segs.length === 0) {                    // 段吃完了
    const index = node.children?.find(c => c.index);
    return index ? [node, index] : [node];    // 有 index 子路由就补上它
  }
  for (const child of node.children ?? []) {
    if (child.index) continue;
    if (child.seg === segs[0]) {              // 这一段对上了
      const sub = matchChain(child, segs.slice(1));   // 继续往深处走
      if (sub) return [node, ...sub];
    }
  }
  return null;                                // 走不下去 → 404
}

// 渲染 = 把链上的 layout 从外到内嵌套, 每层在自己的 <Outlet/> 里放下一层:
//   AppShell( DashboardLayout( SettingsLayout( Profile )))
// 导航到兄弟页, 公共前缀的那几层不卸载 —— 这就是 layout 状态被保留的原因。

2 · 与平表匹配的关系

嵌套匹配可以展开成平表:把每条根到叶的链拼成一条完整 pattern,匹配引擎讲的 segment 种类与优先级规则照样适用。

但树形写法多给了两样东西。其一是 layout 复用,也就是上面那个性质,平表展开后就丢了。其二是关注点分层:鉴权放 dashboard 层、设置页标签放 settings 层,各管一段,不必在每条叶子路由里重复。导航守卫也常按层挂载,进入 dashboard 子树先过登录守卫,就是这棵树上一条边的事。