同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
前言
很多人在做 Vue 项目时,会用 Vue Router 配置路由,但到了动态路由、嵌套路由、多级菜单这些场景,就容易遇到:
- 菜单和路由脱节,手动维护两套配置
- 嵌套层级一多就不知道该写
children还是path - 面包屑要手动维护,改路由结构就要改面包屑
本文会围绕这几个点,讲清楚怎么配置、为什么这样配置、常见坑在哪,目标是写出来就能在实际项目里用得上。
一、先搞清楚:路由和菜单是什么关系?
可以简单理解为:
- 路由:决定访问某个 URL 时展示哪个页面,是“入口”
- 菜单:页面上展示的导航结构,是“入口的入口”
路由和菜单常常是同一份配置的不同展示形式:
一份配置描述“有哪些页面、层级关系”,路由用它来做匹配,菜单用它来渲染。
接下来就从动态路由讲起,然后引出嵌套路由和菜单。
二、动态路由:用占位符匹配不同页面
2.1 什么是动态路由?
比如:/user/1、/user/2 都进同一个组件,只是用户 ID 不同。
这种“路径里有一段是变量”的路由,就是动态路由。
2.2 基本写法
// router/index.js
{
path: '/user/:id', // :id 是动态参数
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue')
}
在组件里获取参数:
// UserDetail.vue
export default {
mounted() {
// 方式1:this.$route.params.id
console.log(this.$route.params.id);
// 方式2:组合式 API
// const route = useRoute();
// console.log(route.params.id);
}
}
2.3 一个常见坑:组件不复用导致不刷新
路由从 /user/1 切到 /user/2 时,如果还是同一个路由组件,Vue 会复用这个组件,mounted 不会再触发,可能拿到的还是旧的 id。
可以用 watch 监听 $route:
export default {
watch: {
'$route'(to) {
// 路由变化时重新拉数据
this.loadUser(to.params.id);
}
}
}
或者用 key 强制重新渲染(不推荐滥用,会有性能开销):
<router-view :key="$route.fullPath" />
小结:动态路由用 :参数名,组件里从 $route.params 取;路由在组件间切换时要特别注意“复用”导致的生命周期不触发问题。
三、嵌套路由:父级是布局,子级才是内容
3.1 为什么需要嵌套路由?
典型布局:顶部导航 + 侧边栏 + 内容区。
- 顶部导航、侧边栏:切换菜单时不变
- 内容区:随菜单切换而变
这种结构用嵌套路由建模:父路由负责布局,子路由负责内容。
3.2 配置方式
// router/index.js
{
path: '/system',
component: Layout, // 布局组件:包含侧边栏 + <router-view/>
children: [
{
path: 'user', // 最终 path 为 /system/user
name: 'SystemUser',
component: () => import('@/views/system/User.vue')
},
{
path: 'role',
name: 'SystemRole',
component: () => import('@/views/system/Role.vue')
}
]
}
布局组件里放一个 <router-view>,子路由对应的组件就会渲染在这里:
<!-- Layout.vue -->
<template>
<div class="layout">
<aside>侧边栏</aside>
<main>
<router-view /> <!-- 子路由组件渲染在这里 -->
</main>
</div>
</template>
3.3 两个容易踩的坑
坑 1:path 要不要加 /
path: 'user':相对于父路由,最终是/system/userpath: '/user':绝对路径,最终是/user,和父路由无关
嵌套路由里,一般子路由用相对路径(不加前导 /)。
坑 2:子路由的 path: '' 作为默认子路由
想让 /system 一进去就显示某个子页面,可以这样写:
{
path: '/system',
component: Layout,
children: [
{
path: '', // 空字符串 = 默认子路由
name: 'SystemHome',
component: () => import('@/views/system/Home.vue')
},
{
path: 'user',
name: 'SystemUser',
component: () => import('@/views/system/User.vue')
}
]
}
这样访问 /system 会渲染 SystemHome,/system/user 会渲染 SystemUser。
四、多级菜单 + 路由:一份配置,自动生成
多级菜单的本质是多层嵌套路由,可以在一份路由配置里同时描述层级和菜单信息,再由代码自动生成菜单。
4.1 路由配置加 meta:用于菜单和权限
// router/index.js
export default [
{
path: '/system',
component: Layout,
redirect: '/system/user', // 默认重定向到第一个子路由
meta: {
title: '系统管理',
icon: 'Setting'
},
children: [
{
path: 'user',
name: 'SystemUser',
component: () => import('@/views/system/User.vue'),
meta: { title: '用户管理', icon: 'User' }
},
{
path: 'role',
name: 'SystemRole',
component: () => import('@/views/system/Role.vue'),
meta: { title: '角色管理', icon: 'Role' }
},
{
path: 'menu',
name: 'SystemMenu',
component: Layout, // 二级菜单也有自己的布局
meta: { title: '菜单管理', icon: 'Menu' },
children: [
{
path: 'list',
name: 'MenuList',
component: () => import('@/views/system/menu/List.vue'),
meta: { title: '菜单列表' }
}
]
}
]
}
]
4.2 从路由自动生成菜单
思路:遍历路由,过滤掉不展示的(如 meta.hidden),递归处理 children。
<!-- components/Sidebar.vue -->
<template>
<aside class="sidebar">
<template v-for="route in menuRoutes" :key="route.path">
<!-- 有子菜单:展示为可展开项 -->
<el-sub-menu v-if="route.children?.length" :index="route.path">
<template #title>
<span>{{ route.meta?.title || route.name }}</span>
</template>
<sidebar-item :routes="route.children" />
</el-sub-menu>
<!-- 无子菜单:展示为可点击项 -->
<el-menu-item v-else :index="resolvePath(route)" @click="go(route)">
{{ route.meta?.title || route.name }}
</el-menu-item>
</template>
</aside>
</template>
<script>
import { useRouter, useRoute } from 'vue-router'
export default {
name: 'Sidebar',
props: {
routes: {
type: Array,
default: () => []
}
},
setup() {
const router = useRouter()
const route = useRoute()
// 过滤出需要展示的菜单项(可加权限等条件)
const menuRoutes = computed(() => {
return filterMenus(props.routes)
})
function filterMenus(routes) {
return routes.filter(r => !r.meta?.hidden).map(r => {
if (r.children?.length) {
return { ...r, children: filterMenus(r.children) }
}
return r
})
}
function resolvePath(item) {
// 若 path 以 / 开头则是绝对路径,否则需要拼接父级
if (item.path.startsWith('/')) return item.path
// 简化:实际应递归拼接父 path,这里假设已在正确层级
return '/' + item.path
}
function go(item) {
router.push(resolvePath(item))
}
return { menuRoutes, resolvePath, go }
}
}
</script>
使用时传入 router.options.routes 或过滤后的路由树即可。
resolvePath 实际项目中要按父子层级正确拼接,避免路径错误。
五、多级面包屑:从当前路由回溯出层级
面包屑要展示“当前页面的完整层级”,例如:系统管理 > 菜单管理 > 菜单列表。
5.1 思路
- 当前路由在
$route.matched里,matched是从根到当前路由的数组 - 对
matched做过滤、映射,就能得到面包屑数组
5.2 示例实现
<!-- components/Breadcrumb.vue -->
<template>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="item.path"
>
<router-link v-if="index < breadcrumbList.length - 1" :to="item.path">
{{ item.meta?.title || item.name }}
</router-link>
<span v-else>{{ item.meta?.title || item.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
import { useRoute } from 'vue-router'
import { computed } from 'vue'
export default {
setup() {
const route = useRoute()
const breadcrumbList = computed(() => {
const matched = route.matched.filter(item => item.meta?.title)
return matched
})
return { breadcrumbList }
}
}
</script>
route.matched 已经按层级排好序,只过滤掉没有 title 的项即可,无需再手动维护面包屑配置。
5.3 可选:首项固定为「首页」
const breadcrumbList = computed(() => {
const matched = route.matched.filter(item => item.meta?.title)
return [{ path: '/', meta: { title: '首页' } }, ...matched]
})
六、一个完整的简化示例
把路由、布局、菜单、面包屑串起来,形成可复制的结构。
6.1 路由配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('@/layout/index.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '首页', icon: 'Home' }
},
{
path: 'system',
name: 'System',
component: () => import('@/layout/index.vue'), // 可复用布局
redirect: '/system/user',
meta: { title: '系统管理', icon: 'Setting' },
children: [
{
path: 'user',
name: 'SystemUser',
component: () => import('@/views/system/User.vue'),
meta: { title: '用户管理' }
},
{
path: 'role',
name: 'SystemRole',
component: () => import('@/views/system/Role.vue'),
meta: { title: '角色管理' }
}
]
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
6.2 布局组件(含侧边栏 + 面包屑 + 内容区)
<!-- layout/index.vue -->
<template>
<div class="layout">
<aside>
<Breadcrumb />
<Sidebar :routes="menuRoutes" />
</aside>
<main>
<router-view />
</main>
</div>
</template>
<script>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import Breadcrumb from './Breadcrumb.vue'
import Sidebar from './Sidebar.vue'
export default {
components: { Breadcrumb, Sidebar },
setup() {
const router = useRouter()
const menuRoutes = computed(() => router.options.routes[0].children || [])
return { menuRoutes }
}
}
</script>
这里只是示意,实际中可能需要根据当前激活的路由层级,只展示对应层级的路由给侧边栏。
七、常见问题速查
| 问题 | 可能原因 | 建议 |
|---|---|---|
| 切换动态路由参数,页面不刷新 | 组件被复用,生命周期未重新执行 | 用 watch 监听 $route 或合理使用 key |
| 子路由 404 | 父路由未写 component 或没有 <router-view> | 检查布局组件是否有 <router-view> |
| 菜单和路由不一致 | 菜单单独维护,未从路由生成 | 用路由 + meta 自动生成菜单 |
| 面包屑层级错乱 | 手动维护或未用 route.matched | 用 route.matched 自动生成 |
八、小结
- 动态路由:用
:id占位,在$route.params中取参,注意组件复用导致的不刷新问题。 - 嵌套路由:父路由负责布局 +
<router-view>,子路由用相对path。 - 多级菜单:在路由
meta中写title、icon等,用递归组件从路由树生成菜单。 - 多级面包屑:用
route.matched自动生成,无需单独维护。
按这个思路,可以在项目中做到:一份路由配置 → 自动生成菜单 + 面包屑,维护成本低,也更容易保持一致。
🔍 本系列专栏导航
一、《路由与布局骨架篇:Vue Router 实战 | 动态路由、嵌套路由与多级菜单》
二、《路由与布局骨架篇:登录态与路由守卫 | token 校验、白名单、重定向》
三、《路由与布局骨架篇:多标签页(Tab)与缓存 | keep-alive、includeexclude、路由 meta》
四、《路由与布局骨架篇:布局系统 | 头部、侧边栏、内容区、面包屑的拆分与复用》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~