在单页应用里,"页面"不再是整屏刷新,而是由路由驱动的组件树。当业务复杂到「用户中心 → 个人资料 / 收货地址 / 账号安全 / 好友列表」这种层级时,嵌套路由(Nested Routes)是唯一能把深度与可维护性同时保留下来的方案。
一、嵌套路由到底在解决什么问题
想象一个用户中心:
bash
/user 用户中心外壳(Layout)
├── /user/profile 个人资料
├── /user/address 收货地址
├── /user/security 账号安全
└── /user/friends 好友列表
如果写成平级路由,每切换一个子页面就要重新加载整个外壳(导航、侧边栏、用户信息),浪费、卡顿、状态丢失。
嵌套路由让外壳只挂载一次,子页面作为 <router-view>
的局部插槽渲染,完美复用外壳,并天然支持面包屑、标签页、权限控制。
二、一条代码看全貌
js
// router/index.js
const routes = [
{
path: '/user',
component: () => import('@/views/user/Layout.vue'), // 外壳
children: [
{ path: '', component: () => import('@/views/user/Profile.vue') },
{ path: 'address', component: () => import('@/views/user/Address.vue') },
{ path: 'security', component: () => import('@/views/user/Security.vue') },
{ path: 'friends', component: () => import('@/views/user/Friends.vue') }
]
}
]
要点:
- 层级关系 = 文件系统:父路由的
component
是文件夹,children
是里面的文件。 - 默认子路由 = 空字符串
''
,访问/user
时自动渲染Profile
。 - 路径写法 = 相对路径:
address
会自动拼接成/user/address
,无需手写全量。
三、Layout 组件
vue
<!-- views/user/Layout.vue -->
<template>
<div class="user-center">
<aside>
<router-link to="/user">个人资料</router-link>
<router-link to="/user/address">收货地址</router-link>
<router-link to="/user/security">账号安全</router-link>
<router-link to="/user/friends">好友列表</router-link>
</aside>
<main>
<router-view /> <!-- 子路由插在这里 -->
</main>
</div>
</template>
子页面渲染时,Layout 组件不会重新创建,导航高亮、用户信息、WebSocket 连接全部保持。
四、动态路由 + 嵌套:URL 即状态
把用户 ID 塞进路径:
js
{
path: '/user/:id',
component: () => import('@/views/user/Layout.vue'),
props: true, // 把 id 作为 prop 注入 Layout
children: [
{ path: '', component: () => import('@/views/user/Profile.vue'), props: true },
{ path: 'address', component: () => import('@/views/user/Address.vue'), props: true }
]
}
访问 /user/42/address
时:
Layout
通过props.id
拿到 42,去拉用户信息;Address
通过props.id
再去拉地址列表;- 切换子路由只改后半段,外壳复用,接口只增不重复。
五、项目实践
1.权限与面包屑
js
{
path: '/user',
component: Layout,
meta: { title: '用户中心', needAuth: true },
children: [
{ path: '', meta: { title: '个人资料' } },
{ path: 'address', meta: { title: '收货地址' } }
]
}
全局后置钩子:
js
router.afterEach(to => {
document.title = to.matched
.map(r => r.meta.title)
.filter(Boolean)
.join(' - ')
})
matched
数组从根到当前节点依次展开,天然就是面包屑数据源。
权限同理:在导航守卫里检查 to.matched.some(r => r.meta.needAuth)
,一次递归即可拿到所有层级要求。
2.代码分割
- 父路由同步加载:外壳体积小,保证首屏骨架秒出;
- 子路由全部懒加载:利用魔法注释给 chunk 命名,方便 CDN 缓存。
js
component: () =>
import(/* webpackChunkName: "user-security" */ '@/views/user/Security.vue')
六、常见问题
- 空路径与斜杠:
path: ''
与path: '/'
都匹配/user
,但后者会额外触发重定向,导致外壳重复渲染。 - 深度监听失效:在
Layout
里watch $route
时,记得加immediate: true
,否则首次进入不触发。 - 滚动位置丢失:给
<router-view>
加key="$route.fullPath"
可强制重新挂载,但会破坏缓存;更优解是在activated
钩子里手动恢复 scrollTop。