嵌套路由与 layout
匹配引擎里,一条 path 命中一条路由、得到一个 handler。真实前端(React Router、Vue Router、Next.js)的路由是一棵树:URL 的每一段对应树里的一层,一条 path 命中的是从根到叶的一整条链。链上每个节点都可以带一个 layout,它们层层嵌套地包住最终的页面——外层画框架(顶栏、侧边栏),内层在它留出的 outlet 槽位里渲染下一层。
这带来一个平表给不了的性质:切换兄弟页面时,共享的外层 layout 原地不动。从 /dashboard/settings/profile 跳到 /dashboard/settings/billing,顶栏、侧边栏与设置页的标签条全部保持挂载,滚动位置、展开状态与输入都不丢,只有最内层那一格被换掉。
/dashboard/settings/profile 与 /dashboard/settings/billing 之间来回点,观察前三层始终标为复用。复用是嵌套路由最值钱的性质。在 profile 与 billing 之间切换时,命中链的前三层(AppShell、DashboardLayout、SettingsLayout)完全相同,框架不卸载也不重建它们,侧边栏的展开状态与标签条位置都原样保留,只有最内层被换下。
1 · index 路由
/dashboard 命中了 DashboardLayout,但 layout 只是个带 outlet 的框,它那一格该显示什么?答案是 index 路由:一条没有自己 path 段、专门填充父 layout 默认内容的子路由,对应 React Router 的 index 与 Vue Router 的默认子路由。所以
/dashboard 实际命中链是 AppShell → DashboardLayout → Overview,三层,最后那层是 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 子树先过登录守卫,就是这棵树上一条边的事。