路由与布局骨架篇:布局系统 | 头部、侧边栏、内容区、面包屑的拆分与复用

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。

一、先搞清楚一件事:什么是布局?

布局(Layout)就是页面里不随路由变的那一部分 :头部、侧边栏、面包屑、底部等。

真正随路由变化的是「内容区」。布局负责把这些固定区域包起来,内容区填进其中。

  • 布局:结构固定、多页面共用
  • 内容区:随路由切换、每页不同

理解了这一点,再去看 Vue Router 的嵌套路由,就很好理解。

二、为什么要拆分布局组件?

不拆的话,每个页面都要写一遍头部、侧边栏,会有这些问题:

  1. 重复代码多
  2. 改头部要改 N 个页面
  3. 页面结构和布局混在一起,难维护

拆分后:

  • 布局组件:只负责头部、侧边栏等固定结构
  • 内容区:只负责当前页面的业务
  • 路由:负责决定「用哪个布局」「在哪个槽位渲染内容」

三、整体结构预览

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

侧边栏菜单需要和路由保持一致,用 routerroutes 或自己维护菜单配置都可以。

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 绑定当前路径,实现高亮

面包屑需要从当前路由推导出层级,用 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 菜单导航 使用 routerroute.path 高亮
Breadcrumb 当前路径层级展示 依赖 route.matchedmeta
内容区 子页面内容 <router-view> 渲染

记住三步:

  1. 用嵌套路由,父用 Layout,子用具体页面
  2. 布局拆成 Header、Sidebar、Breadcrumb、router-view 四个区域
  3. 菜单、面包屑都从 routemeta 推导,避免重复配置

如果你希望我把某个小节展开(例如只用原生 div + CSS,或用 Vue 2 + Vue Router 3 版本),可以说一下具体需求,我可以再补一版对应示例。

🔍 本系列专栏导航

一、《路由与布局扫盲篇:Vue Router 实战 | 动态路由、嵌套路由与多级菜单》

二、《路由与布局扫盲篇:登录态与路由守卫 | token 校验、白名单、重定向》

三、《路由与布局扫盲篇:多标签页(Tab)与缓存 | keep-alive、includeexclude、路由 meta》

四、《路由与布局扫盲篇:布局系统 | 头部、侧边栏、内容区、面包屑的拆分与复用》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

相关推荐
代码煮茶1 小时前
前端网络请求实战 | Axios 从入门到封装(拦截器 / 错误处理 / 重试)
javascript
进击的尘埃1 小时前
组合式函数 Composables 的设计模式:如何写出可复用的 Vue3 Hooks
javascript
大金乄1 小时前
用canvans画一个流程图
前端
大金乄1 小时前
TreeSelect 是一个基于 Element UI 的树形选择器组件,结合了 el-select 和 el-tree 的功能,支持单选和多选模式,支持树形
前端
最强僚机斯内克2 小时前
Vue 3 + Vite 自动引入插件完整指南(unplugin-vue-components,unplugin-auto-import)
vue.js
大金乄2 小时前
自动构建打包脚本(开发环境)
前端
进击的尘埃2 小时前
浏览器渲染管线深度拆解:从 Parse HTML 到 Composite Layers 的每一帧发生了什么
javascript
大雨还洅下2 小时前
前端手写: Promise封装Ajax
javascript
jerrywus2 小时前
为什么每个程序员都应该试试 cmux:AI 加持的终端效率革命
前端·人工智能·claude