Skip to content

05|嵌套路由与布局 ​

第六章概览 / 05。目标:children 描述嵌套 matched records,父组件中的下一层 <RouterView> 才能显示子组件。

路由树与组件树一一对应 ​

ts
const routes = [
  {
    path: '/settings',
    component: () => import('./layouts/SettingsLayout.vue'),
    children: [
      { path: '', redirect: { name: 'settings-profile' } },
      { path: 'profile', name: 'settings-profile', component: () => import('./views/ProfileView.vue') },
      { path: 'security', name: 'settings-security', component: () => import('./views/SecurityView.vue') },
    ],
  },
]

child path profile 没有开头 /,会与父路径组合成 /settings/profile。空 child '' 在访问父路径时匹配,可渲染默认页或重定向。child path 若以 / 开头,会成为绝对 URL,但 matched record 仍嵌在父级;除非确有这种布局需求,否则容易让 URL 层级和组件层级难以理解。

vue
<!-- SettingsLayout.vue -->
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <section>
    <h1>设置</h1>
    <nav aria-label="设置导航">
      <RouterLink :to="{ name: 'settings-profile' }">资料</RouterLink>
      <RouterLink :to="{ name: 'settings-security' }">安全</RouterLink>
    </nav>
    <RouterView />
  </section>
</template>

顶层 App 的 RouterView 渲染 SettingsLayout;布局内的 RouterView 渲染 profile/security。少一个出口不会自动把子组件挤到别处,只会看到父布局而内容为空。

route.matched 是实际命中的记录链 ​

访问 /settings/security 时,route.matched 依次包含父 record 和 security child。route.meta 是这条链 meta 的非递归合并结果;面包屑等需要逐级信息时遍历 matched,不要只看最后一条。

无组件父 record 可用于给一组 URL 共享前缀/守卫/meta;需要视觉壳、局部导航或共享资源时才提供 layout component 与 RouterView。不要为目录结构机械增加布局层。

例如从profile到security,可以按深度逐层核对:深度0仍命中settings父record,App出口保留SettingsLayout;深度1从profile记录变为security记录,布局出口替换子组件。只有一个顶层RouterView时,第一步已经完成,第二步却没有出口,所以“路由匹配成功但内容空白”完全可能发生。先检查 router.resolve('/settings/security').matched.map(record => record.path),再检查模板中的出口,比反复调整path可靠。

给父布局与两个子页分别在setup打印自己的名字,再切换两次。正常情况父布局只打印一次,两个子页在各自新建时打印;若把整个App出口的key设为fullPath,父布局也重复打印,证明复用被调用方主动取消。页面草稿是否保留取决于哪个实例活着,不能仅凭URL有共同前缀作判断。

父布局为什么会保留 ​

在 /settings/profile 和 /settings/security 间切换,父 record 未离开,SettingsLayout 实例通常复用;子页面卸载/挂载。这使布局的滚动、订阅和局部状态可以跨子页保留,但也带来 owner 责任:

  • 设置页共同草稿可以由 layout 拥有。
  • 只属于 Profile 的输入留在 Profile,离开即清理。
  • 用户 id 等父参数改变时,布局可能复用,需 onBeforeRouteUpdate 或 watch 明确刷新。
  • 用 :key="route.fullPath" 强制重建顶层组件会破坏这种复用,除非这是产品要求。

参数继承与 props ​

ts
{
  path: '/users/:userId',
  component: UserLayout,
  children: [
    {
      path: 'posts/:postId',
      name: 'user-post',
      component: PostView,
      props: route => ({
        userId: String(route.params.userId),
        postId: String(route.params.postId),
      }),
    },
  ],
}

child 可以读取父/子参数。把业务页面所需值作为 props 传入,可在不创建 Router 的组件测试中验证;但解析和格式校验仍要有明确位置。

守卫与失败边界 ​

父 route 的 beforeEnter 只在进入该 record 时运行;在同一父级的 children 之间切换不会再次运行。只改 /users/:id 的参数时,route record 也可能仍是同一条,beforeEnter 不负责刷新数据;页面应 watch 参数或使用 onBeforeRouteUpdate。

父布局加载成功、子懒加载失败时,导航会作为错误结束;应用需要全局 router.onError 恢复入口,不能把空 RouterView 当成普通空状态。

常见失败路径 ​

  • 配置 children 但父组件没有 RouterView:URL 正确、子内容空白。
  • child 写成 /profile 却以为得到 /settings/profile:开头 / 表示绝对路径。
  • 父路由和默认 child 都取名,并只导航父 name:命名目标只指向对应 record,不保证默认 child 如预期装配;对最终页面使用明确 child name。
  • 在父 beforeEnter 加数据刷新:子页切换/参数变化时不重跑。
  • 所有目录层都变路由层:matched/meta/守卫复杂度增加,却没有界面层级价值。

练习:嵌套路由布局与局部 404 ​

配置 /sections/:sectionId 布局,默认重定向 overview,另有 items 子页。布局显示区段标题和子导航,子页面通过 props 接收 sectionId。验证:两个子页切换时布局实例不重建;sectionId 从 a 变 b 时重新读取区段;未知 child 进入当前区段范围内的 404 页面。题目只验证嵌套路由、参数复用和局部错误出口,不要求设计业务数据模型。

变化题:把 tasks 改成绝对 path,预测 URL 与布局是否仍嵌套,并说明为什么不推荐在这里使用。

参考答案:第 05 题。

本页结论 ​

嵌套路由是 matched record 树,不只是 URL 缩进。父出口、child path、实例复用和参数刷新要一起设计。

用心记录每一步成长