同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。
一、先搞清楚一件事:什么是布局?
布局(Layout)就是页面里不随路由变的那一部分 :头部、侧边栏、面包屑、底部等。
真正随路由变化的是「内容区」。布局负责把这些固定区域包起来,内容区填进其中。
- 布局:结构固定、多页面共用
- 内容区:随路由切换、每页不同
理解了这一点,再去看 Vue Router 的嵌套路由,就很好理解。
二、为什么要拆分布局组件?
不拆的话,每个页面都要写一遍头部、侧边栏,会有这些问题:
- 重复代码多
- 改头部要改 N 个页面
- 页面结构和布局混在一起,难维护
拆分后:
- 布局组件:只负责头部、侧边栏等固定结构
- 内容区:只负责当前页面的业务
- 路由:负责决定「用哪个布局」「在哪个槽位渲染内容」
三、整体结构预览
sql
Layout(布局容器)
├── AppHeader(头部)
├── AppSidebar(侧边栏)
├── Breadcrumb(面包屑,可选)
└── 内容区(由 <router-view> 渲染)
接下来按「路由配置 → 布局组件 → 各子组件」的顺序说明。
四、路由配置:布局与路由如何配合?
核心思路:用嵌套路由,父路由用 Layout,子路由占内容区。
4.1 基础路由结构
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/layouts/BasicLayout.vue'
const routes = [
{
path: '/',
component: Layout, // 父路由使用布局组件
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '仪表盘', icon: 'dashboard' }
},
{
path: 'user',
name: 'User',
component: () => import('@/views/User.vue'),
meta: { title: '用户管理', icon: 'user' }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
要点:
- 父路由
path: '/'用Layout children里的每个路由才是具体页面meta用来存标题、图标等,后面给面包屑和菜单用
4.2 多个布局怎么办?
例如:后台用带侧边栏的布局,登录页用简单布局。
javascript
const routes = [
// 后台布局(带侧边栏)
{
path: '/',
component: () => import('@/layouts/BasicLayout.vue'),
children: [
{ path: 'dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '仪表盘' } },
{ path: 'user', component: () => import('@/views/User.vue'), meta: { title: '用户管理' } }
]
},
// 登录页布局(无侧边栏)
{
path: '/login',
component: () => import('@/layouts/BlankLayout.vue'),
children: [
{ path: '', component: () => import('@/views/Login.vue') }
]
}
]
每个布局对应一个父路由,它的 children 共用同一个布局。
五、布局组件 BasicLayout.vue
5.1 完整示例
html
<!-- layouts/BasicLayout.vue -->
<template>
<el-container class="basic-layout">
<!-- 头部 -->
<AppHeader />
<el-container>
<!-- 侧边栏 -->
<AppSidebar />
<!-- 主内容区 -->
<el-main class="main-content">
<!-- 面包屑 -->
<Breadcrumb />
<!-- 内容区:由路由渲染 -->
<div class="content-wrapper">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import AppHeader from './components/AppHeader.vue'
import AppSidebar from './components/AppSidebar.vue'
import Breadcrumb from './components/Breadcrumb.vue'
</script>
<style scoped>
.basic-layout {
min-height: 100vh;
flex-direction: column;
}
.main-content {
padding: 20px;
background: #f5f7fa;
}
.content-wrapper {
margin-top: 16px;
padding: 20px;
background: #fff;
border-radius: 4px;
min-height: calc(100vh - 180px);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
要点:
<router-view>就是子路由渲染的地方v-slot="{ Component }"+<component :is="Component">可以配合过渡动画- 没有用 Element Plus 的话,把
el-container换成普通 div 即可
六、各子组件实现
6.1 头部 AppHeader.vue
html
<!-- layouts/components/AppHeader.vue -->
<template>
<header class="app-header">
<div class="header-left">
<span class="logo">后台管理系统</span>
</div>
<div class="header-right">
<span class="user-name">管理员</span>
<button @click="handleLogout">退出</button>
</div>
</header>
</template>
<script setup>
const handleLogout = () => {
// 登出逻辑
console.log('退出登录')
}
</script>
<style scoped>
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
padding: 0 24px;
background: #001529;
color: #fff;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
</style>
6.2 侧边栏 AppSidebar.vue
侧边栏菜单需要和路由保持一致,用 router 的 routes 或自己维护菜单配置都可以。
html
<!-- layouts/components/AppSidebar.vue -->
<template>
<aside class="app-sidebar">
<el-menu
:default-active="activeMenu"
router
background-color="#001529"
text-color="#fff"
>
<el-menu-item index="/dashboard">
<span>仪表盘</span>
</el-menu-item>
<el-menu-item index="/user">
<span>用户管理</span>
</el-menu-item>
</el-menu>
</aside>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 高亮当前路由对应的菜单项
const activeMenu = computed(() => route.path)
</script>
<style scoped>
.app-sidebar {
width: 200px;
background: #001529;
}
</style>
要点:
router属性:点击菜单项会直接router.push(index),无需手动处理default-active绑定当前路径,实现高亮
6.3 面包屑 Breadcrumb.vue
面包屑需要从当前路由推导出层级,用 route.matched 即可。
html
<!-- layouts/components/Breadcrumb.vue -->
<template>
<el-breadcrumb separator="/" class="breadcrumb">
<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 setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 从路由的 matched 自动生成面包屑
const breadcrumbList = computed(() => {
return route.matched.filter(item => item.meta?.title || item.name)
})
</script>
<style scoped>
.breadcrumb {
margin-bottom: 16px;
}
</style>
要点:
route.matched是当前路由及其所有父路由的数组,正好对应面包屑层级- 最后一项用
<span>,前面的用<router-link>方便点击返回
七、常见坑点
坑 1:侧边栏和路由不同步
- 原因:菜单写死在模板里,路由改了菜单没改
- 做法:用
router.options.routes或单独维护菜单配置,和路由保持一致,用route.path作为菜单的index
坑 2:面包屑不显示或显示不对
- 原因:
route.matched里的路由没有meta.title - 做法:给每个需要出现在面包屑中的路由加上
meta: { title: 'xxx' },根路由如果是 redirect 可以不加或设为hidden: true
坑 3:刷新后侧边栏高亮错误
- 原因:
default-active没正确绑定到当前路径 - 做法:用
computed(() => route.path)绑定,并且菜单项的index和路由的path一致
坑 4:布局组件被重复创建
- 原因:同一个父路由下的子路由切换时,Vue Router 默认会复用父级 Layout
- 做法:这是正常行为。若需要在切换子路由时强制重挂载 Layout,可以给
router-view加:key="route.fullPath",但一般不需要
八、菜单与路由统一:进阶写法
为了不重复维护「路由」和「菜单」,可以统一用路由生成菜单:
javascript
// 在 router 里定义好 meta
// 在 AppSidebar 里动态读取
import { useRouter } from 'vue-router'
const router = useRouter()
const menuRoutes = computed(() => {
const parent = router.options.routes.find(r => r.path === '/')
return (parent?.children || []).filter(r => !r.meta?.hidden)
})
html
<el-menu-item
v-for="item in menuRoutes"
:key="item.path"
:index="'/' + item.path"
>
{{ item.meta?.title }}
</el-menu-item>
这样菜单和路由只维护一份。
九、总结
| 模块 | 职责 | 与路由的关系 |
|---|---|---|
| Layout | 包裹头部、侧边栏、内容区 | 作为父路由的 component |
| Header | 顶部固定区域 | 一般与路由无关 |
| Sidebar | 菜单导航 | 使用 router、route.path 高亮 |
| Breadcrumb | 当前路径层级展示 | 依赖 route.matched 和 meta |
| 内容区 | 子页面内容 | 由 <router-view> 渲染 |
记住三步:
- 用嵌套路由,父用 Layout,子用具体页面
- 布局拆成 Header、Sidebar、Breadcrumb、
router-view四个区域 - 菜单、面包屑都从
route和meta推导,避免重复配置
如果你希望我把某个小节展开(例如只用原生 div + CSS,或用 Vue 2 + Vue Router 3 版本),可以说一下具体需求,我可以再补一版对应示例。
🔍 本系列专栏导航
一、《路由与布局扫盲篇:Vue Router 实战 | 动态路由、嵌套路由与多级菜单》
二、《路由与布局扫盲篇:登录态与路由守卫 | token 校验、白名单、重定向》
三、《路由与布局扫盲篇:多标签页(Tab)与缓存 | keep-alive、includeexclude、路由 meta》
四、《路由与布局扫盲篇:布局系统 | 头部、侧边栏、内容区、面包屑的拆分与复用》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~