“RBAC前端架构-08”这个名字,看着像是系列文章里的某一篇,光说“页面骨架Layout布局搭建”,可能很多人觉得不就是套个后台模板吗?侧边栏、顶栏、内容区,脚手架一堆,换换样式就完事了。但真正动手写RBAC系统的时候你会发现,Layout是整个权限体系里最容易被低估的一层。菜单不是写死的,路由是按角色动态挂载的,标签页要能缓存,面包屑要能反推层级,侧边栏的选中状态还得跟着路由走。这些东西一股脑全压在“页面骨架”四个字上,处理得好不好,直接决定后续业务页面开发是顺畅还是天天在跟导航死磕。
这篇文章不聊怎么把界面做得好看,只聊骨架的工程化搭建。我会从Layout在大前端里的定位、三栏核心组件怎么拆、菜单与路由如何联动、动态路由如何配合权限、标签页与缓存怎么处理这几个维度,逐步带你搭出一套能支撑RBAC体系的页面骨架。适合正在做后台管理系统、尤其是正在从静态页面往动态权限体系过渡的同学参考,也适合团队里刚开始负责前端技术基建的同学读一读,看完能少走不少弯路。
1. 为什么说Layout是RBAC前端的门面工程
1.1 很多人低估了静态页面骨架背后的问题
先回忆一下最常见的后台管理页面长什么样:左侧菜单,顶部栏,右侧内容区。如果是带标签页的,顶部或者菜单下方还会有一排“多页面签”。大部分后台脚手架,比如vue-element-admin、Ant Design Pro,都是这个结构。第一次接触的人觉得“这不就是布局组件嘛”,把模板拿过来改改,往里套页面就行。但RBAC系统真正跑起来的时候,问题就开始冒头了。
我一开始也觉得Layout没什么好说的,直到遇到一个需求:同一个系统里,A角色登录只能看到“订单管理”,B角色登录能看到“订单管理”“用户管理”“权限配置”,而且两个角色都访问同一个URL时,侧边栏的菜单要不同,路由表里可访问的页面也要不同。这才意识到,所谓“页面骨架”,根本不是一个简单的外壳,它是一个需要感知当前用户是谁、当前角色能干什么、当前路由走到了哪里的“权限执行层”。
RBAC的R(Role)最终是要落到U(User)、P(Permission)上的,而前端体现权限最直观的地方,就是Layout里的菜单和路由。如果骨架本身没有设计好,后面权限控制做得再细,菜单和路由对不上,页面就会出现“用户能通过URL直接跳到一个角色本来不该访问的页面”这种尴尬情况。
1.2 Layout骨架决定RBAC的目录化思维
再往深处说,Layout骨架实际上是在为整个系统搭“目录”。你想想后台系统的使用习惯,用户进到系统里,第一眼看到的是侧边栏菜单,这是他对整个系统的第一个认知。菜单的层级、分组、顺序,本质上就是一套信息架构。RBAC里的角色权限,在前端最直观的表现就是“这套目录在不同角色眼里长什么样”。所以Layout不只是在渲染导航,它是在做“权限控制的前排展示”。
从工程角度讲,Layout骨架做得好,后续的权限指令、动态路由、页面缓存、面包屑导航都会顺畅很多。反过来,如果一开始就直接写死菜单,后面要接动态权限,差不多要把整个导航组件推翻重写。我在这块重新折腾过两次,体会非常深。所以这篇文章会花比较多的篇幅讲“骨架与路由、权限的关系”,而不是单纯讲样式。
2. Layout选型对比:Element Plus布局、ProLayout与自定义骨架
2.1 直接使用UI库布局还是完整脚手架?
做布局选型的时候,首先要分清楚一个概念:Element Plus提供的el-container是一套布局容器组件,它能帮你把侧边栏、头部、内容区拼起来,但菜单、路由联动、权限过滤这些逻辑,它一概不管。Ant Design Pro则是一个完整的脚手架,自带了ProLayout、菜单权限、路由生成,开箱即用,但同时也意味着你要跟随它的约定,灵活性上会有一定妥协。
我在几个项目里的实践结论是这么定的:如果团队用的是Vue3 + Element Plus这套技术栈,而且项目刚好需要深度定制菜单和权限逻辑,不建议引入太重型的脚手架,直接用el-container或者自己写布局结构,把核心逻辑掌握在项目自己手里。如果团队用的是React,而且项目进度非常赶,直接用Ant Design Pro是性价比最高的选择,它的ProLayout已经把菜单、面包屑、页签这些做得很完善,你只需要把数据源接好。
如果你问我自己动手搭和用现成脚手架的判断标准是什么,我会这么看:关键看你未来要不要在导航层面做深度定制。比如菜单项需要根据按钮权限动态显隐,或者同一个菜单在不同角色下有不用的灰色状态,又或者你需要把菜单维度做成从后端接口下发,这时候自研骨架反而比改造现成脚手架更划算。
2.2 三栏布局的形态选择与适合场景
Layout布局形态,常见的有三种。第一种是固定侧边栏,也就是侧边栏永远展开,菜单完整展示,适合大部分后台管理场景,信息层级一目了然。第二种是可折叠侧边栏,窄屏上把菜单折叠成图标模式,适合需要经常切换菜单功能区的系统,但折叠态下菜单层级不宜太深,否则交互会比较繁琐。第三种是混合布局,顶部放一级菜单,侧边栏放二级菜单,适合业务模块特别多、一级菜单超过七个的项目。整体结构比较重,维护成本也高,需要结合具体业务量级评估。
我在实际项目中通常优先选“固定侧边栏 + 窄屏自动折叠”的组合,代码简单,交互也符合直觉。给个简单的结构示例:
<el-container class="layout-wrapper"> <el-aside :width="isCollapse ? '64px' : '220px'"> <Sidebar /> </el-aside> <el-container> <el-header> <HeaderBar /> </el-header> <el-main> <router-view /> </el-main> </el-container> </el-container>这只是最粗的骨架。接下来真正花时间的,是怎么把侧边栏、顶部栏、内容区三个区域拆出合理组件,以及怎么让它们和路由、权限系统对接。
3. 骨架拆解:侧边栏、顶栏、内容区三件套
3.1 侧边栏组件:不止是渲染菜单
侧边栏是Layout里最核心的组件,因为它承载了权限系统最直观的部分——菜单。我这里说的不止是el-menu本身,而是“根据路由表动态生成菜单”的一套逻辑。
先讲菜单的数据从哪来。很多人会单独定义一个menu.js,把菜单写死在数组里,然后传给侧边栏渲染。这种做法在项目很小的时候没问题,但一旦接上动态路由,很容易出现“路由表一份、菜单一份,两边要手动同步”的维护噩梦。我的做法是:菜单数据不要单独维护,直接基于路由表去生成,让路由表成为唯一数据源。这样你新增一个页面只需要在路由表里加一条配置,菜单自动多一项,删除同理。
路由表字段上,我在每个路由配置里约定了一组meta字段,包括title(菜单名称)、icon(图标)、hidden(是否在菜单隐藏)、roles(允许访问的角色列表)。TypeScript或者JSDoc注释里约定好之后,侧边栏直接递归读取routes,就能生成多级菜单。
{ path: '/order', name: 'Order', component: Layout, redirect: '/order/list', meta: { title: '订单管理', icon: 'Order' }, children: [ { path: 'list', name: 'OrderList', component: () => import('@/views/order/list.vue'), meta: { title: '订单列表' } }, { path: 'detail', name: 'OrderDetail', component: () => import('@/views/order/detail.vue'), meta: { title: '订单详情', hidden: true, activeMenu: '/order/list' } } ] }注意一个细节,detail这个子路由在菜单里是隐藏的,因为订单详情页通常是从列表页跳进来的,不需要在侧边栏里单列一项。但detail页激活的时候,侧边栏需要高亮“订单管理”下的“订单列表”。这个需求靠el-menu的default-active属性是搞不定的,因为default-active对应的是当前路由路径,detail的路径是/order/detail,高亮会落在“订单详情”这个隐藏菜单上。解决办法是给meta加上activeMenu字段,当前路由激活菜单时优先取activeMenu的值。
3.2 顶栏组件:信息层次与全局功能
顶栏在RBAC系统里承载的东西通常比想象的要多。首先要放的是面包屑导航,它能让用户随时知道自己当前在哪个层级、上一级是什么。其次是侧边栏折叠按钮、全屏按钮、用户信息下拉菜单。在RBAC体系里,顶栏还有一个容易被忽视的功能:全局搜索或者快捷入口。当菜单层级很深时,用户找一个功能可能要展开好几层菜单,全局搜索能显著降低操作成本。
我自己实践下来,顶栏组件不要写成“一个大组件包所有”,而是拆成“HeaderBar容器 + 子组件组合”的形式。HeaderBar本身只负责布局,左边放折叠按钮,中间放面包屑,右边放全屏按钮和用户下拉,用户下拉里根据权限项显隐“修改密码”“退出登录”这些入口。这样各功能区独立维护,后期改动成本低。
还有一点,顶栏要能感知“当前路由有没有访问权限”。我记得有个版本,用户直接输入一个无权访问的URL,系统跳到了404页面,但顶栏和侧边栏还是渲染出来了,界面就很奇怪。后来我在Layout的根组件里加了一个权限守卫判断,一旦当前路由的roles和用户权限不匹配,直接渲染一个“无权限访问”页面而不是走默认的404,视觉上合理很多。
3.3 内容区组件:路由出口与过渡效果
内容区是整个Layout里“最没技术含量但最容易出问题”的地方。因为所有业务页面都渲染在这里,页面切换时的状态保持、滚动条处理、过渡动画,都在这一层统一处理。
第一件事是router-view要包一层keep-alive,否则用户从列表页切到详情页再切回来,列表页的筛选条件和当前页码全部丢失,体验非常差。第二件事是过渡动画,我习惯给router-view加一个简单的fade-transform过渡,视觉上顺畅,而且实现成本很低。第三件事是滚动条,内容区要有自己的滚动容器,不能让滚动条跑到了浏览器窗口上,否则侧边栏和顶栏就无法固定。
内容区组件本身不用做太多业务逻辑,但它是下面要讲的“标签页多开、缓存、面包屑”这些高级功能的挂载点。很多时候,路由的meta配置会在这里被读取,决定当前页面要不要被缓存、要不要显示在标签页列表里等内容。
4. 菜单与路由的单源设计:真正难的是联动
4.1 单源原则:菜单从路由表生成
前面提到了路由表作为菜单的唯一数据源,这里展开说说这样设计的理由。最直接的收益是不存在“两套数据”,你只维护routes数组,菜单侧边栏组件递归读取routes就能自动生成。更重要的是,菜单的显隐、跳转、图表,全部跟着路由配置走,权限系统只需要控制routes里哪些项对当前用户可见,菜单和路由天然同步。
具体到代码层面,递归组件是核心。Element Plus的el-menu天然支持嵌套el-sub-menu,所以只要做一个递归遍历,就能把任意层级的routes渲染成菜单。这里给一个递归菜单组件的简单示例,我用的是Vue3 + Composition API的写法:
<template> <template v-for="item in menus" :key="item.path"> <el-sub-menu v-if="!item.meta?.hidden && item.children?.length" :index="resolvePath(item.path)"> <template #title> <el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon> <span>{{ item.meta?.title }}</span> </template> <SidebarItem :menus="item.children" :base-path="resolvePath(item.path)" /> </el-sub-menu> <el-menu-item v-else-if="!item.meta?.hidden" :index="resolvePath(item.path)"> <el-icon v-if="item.meta?.icon"><component :is="item.meta.icon" /></el-icon> <template #title>{{ item.meta?.title }}</template> </el-menu-item> </template> </template>resolvePath负责拼接父级路径和子级路径,避免子项只写了相对路径导致路由跳转错误。这个函数虽然简单,但很多人第一次写递归菜单时会漏掉,结果就是二级菜单路径不对,点哪儿都404。
4.2 菜单选中态不能只依赖currentPath
菜单高亮(default-active)看似简单,但在RBAC系统里会有一些边角情况。比如前面提到的隐藏详情页,还有类似“跳转第三方链接”“跳转新窗口”的菜单项,以及某些页面需要同时高亮两个菜单的场景。
我碰到的实际问题是:标签页多开时,从“订单列表”新窗口打开“数据报表”,侧边栏的选中态应该怎么算?通常的做法是,侧边栏的default-active取当前路由的path,如果meta里有activeMenu,则取activeMenu的值。但新窗口打开的页面其实不在当前标签页里,它不会影响主窗口的选中状态,所以在Layout里监听路由变化时,需要判断是当前窗口路由变化还是新窗口打开,避免误改菜单高亮。
更稳妥的做法是:把“激活菜单路径”设计成计算属性,优先读route.meta.activeMenu,没有就回退到route.path。然后el-menu的:default-active绑定这个计算属性,路由变化时自然跟随。
4.3 多级菜单的层级治理:最多层级不超过三级
多级菜单在功能上能无限嵌套,但使用体验上通常不建议超过三级。我见过一个后台系统,菜单嵌套了五层,用户找一个功能要点五下,非常痛苦。在路由设计阶段,就要对菜单树做层级治理,能平铺的场景尽量平铺,能用顶栏分组归并的就不要全部塞进侧边栏。
如果业务确实需要超过三级的菜单,我会改用混合导航布局:顶部一级菜单对应业务模块,侧边栏展示该模块内部的二级、三级菜单。这样既保留了信息架构的完整,又不会让侧边栏递归过深。
5. 动态路由与角色权限的配合:骨架从静态变动态的关键
5.1 动态路由的挂载时机与前置条件
静态Layout和动态Layout最大的区别,在于routes是不是登录后根据角色权限动态生成的。在RBAC体系里,用户登录之后,前端要先拿到用户的角色信息,然后根据角色去取对应的可访问路由配置,再调用router.addRoute将权限路由挂载上去。
这里有一个先后顺序需要注意:一定要先设置路由,再跳转到目标页面,否则会出现刷新后白屏或者404。正确的流程是:登录成功后先拉取用户信息,拿到roles,调用store里的generateRoutes方法生成权限路由表,循环addRoute,然后router.replace到用户本来想访问的页面。在全局路由守卫里,还需要判断动态路由是否已经挂载,用一个标记变量控制,避免重复执行addRoute。
// 路由守卫简化版 router.beforeEach(async (to, from, next) => { const store = useUserStore() if (!store.token) { if (to.path === '/login') return next() return next(`/login?redirect=${to.fullPath}`) } if (!store.roles.length) { const roles = await store.getUserInfo() const accessRoutes = await store.generateRoutes(roles) accessRoutes.forEach(route => router.addRoute(route)) return next({ ...to, replace: true }) } next() })这块代码看起来简单,但它解决了几个关键的时序问题:刷新页面后动态路由丢不丢、用户直接输URL时能不能正确拦截、addRoute会不会重复执行。
5.2 刷新页面后动态路由丢失:必须把路由状态恢复到当前页
动态路由最大的坑就是刷新页面。因为addRoute是运行时挂载,刷新之后路由表回到初始状态,之前动态加进去的路由全没了。理论上常见做法是把动态路由表存在Pinia或Vuex里,配合持久化插件存到sessionStorage,刷新后重新addRoute。但这里有一个坑:如果刷新前的页面路径是动态路由里的某个子页面,刷新时会先触发全局路由守卫,此时动态路由还没恢复,to.path匹配不到任何路由记录,就会被误判为404。
这个问题我印象太深了。第一次做动态路由,上线第二天就接到一个bug:某个页面刷新直接白屏。排查了很久,最后发现是路由守卫里把“未匹配到路由”直接导到了404,而且动态路由没恢复。后来处理方式是:在每次刷新后先执行“恢复动态路由”的动作,再放行页面访问。恢复完成后,用next(to.fullPath)重新进入一次路由匹配逻辑,确保目标路由可访问之后再放行。
5.3 动态路由场景下404兜底要最后加
动态路由还有一个细节很多人会忽略:404兜底路由的注册顺序。前端路由匹配规则是基于路由树顺序的,如果404通配符路由被最先注册,它会把所有未匹配路径全部接管,动态路由加进来也没用。所以动态路由场景下,路由表拆分要实现静态路由(登录页、404、Layout)、动态路由(业务页面)分开管理。404不能放在动态路由前面,一般建议放在静态路由表的最后一个children里,动态路由挂载时放在最后。
这个坑我项目中遇到过,用户访问一个不存在的路径,却总是跳转到登录页而不是404,排查到最后发现是路由顺序问题。后来我把路由守卫里的无匹配判断改成了:如果动态路由已恢复,且to.path对应不上任何一个已注册路由,才跳404。
6. 标签页多开、缓存与面包屑:细节决定了骨架顺不顺手
6.1 标签页多开:用store管理tab列表
做完菜单和路由联动,Layout已经具备“基本骨架”的能力。但实际后台系统使用中,还有三块直接影响日活体验的功能要处理:标签页(多页面签)、页面缓存、面包屑。
标签页实现的核心是一个tabs数组,存当前已打开的页面路由信息。路由切换时,如果tabs里没有当前路由,就push进去;如果已经有了,就更新标题、路径等字段。关闭标签页时,要判断关闭的是不是当前页,如果是,需要自动激活相邻标签页。
const state = reactive<{ tabs: TabItem[] }>({ tabs: [] }) function addTab(route) { const { path, name, meta, fullPath } = route if (!path.startsWith('/')) return const existing = state.tabs.find(tab => tab.path === path) if (!existing) { state.tabs.push({ path, fullPath, name, title: meta?.title, affix: meta?.affix }) } else { existing.fullPath = fullPath } }这里有一个设计点:判断tab是否重复应该用标签页的唯一标识,通常用path,但如果同一个页面要打开不同参数,比如“用户详情?id=1”和“用户详情?id=2”,path是一样的。要不要区别为两个标签页?我的做法是默认共用一个tab,切换参数时只更新fullPath,这样符合大多数后台系统的习惯。如果业务确实需要同页多开,就得在路由meta里加一个canReuse字段,单独控制。
6.2 KeepAlive缓存必须和组件name对齐
页面缓存是标签页多开后紧接着的问题。如果每个标签页都要保持自己的状态,router-view外面需要包keep-alive,且include要配置为当前所有缓存页面的组件name列表。这个配置有严格的要求:缓存用的名字必须和页面组件的name字段完全一致,否则缓存不生效,而且会报警告。
我项目里遇到过的问题是,页面组件用Vite的defineOptions定义name,但标签页里存的是route.name,两者没对齐。结果部分页面缓存失效。后来统一规范:路由配置里的name和页面组件的name保持一致,标签页store里优先使用组件name,缓存问题自然消失。
keep-alive的include数组需要和tabs列表联动,tabs里新增一个tab,如果meta允许缓存,就把组件name加入include;关闭tab时,从include移除。这样才能做到“关闭标签页后状态释放,打开的标签页状态保留”。
6.3 面包屑的回退逻辑:动态路由下不能只靠meta
面包屑一般基于route.matched生成,每一层取meta.title。但动态路由场景下有一个坑:detail页面在菜单里是隐藏的,它的meta.title是“订单详情”,但用户是从“订单管理 > 订单列表”跳进来的,此时面包屑应该显示“订单管理 / 订单列表 / 订单详情”,还是“订单详情”?前一种更符合操作直觉,但route.matched不会自动给你带上父级“订单列表”。
我针对这种情况做了一个扩展:在路由meta里加一个crumbFrom,值为目标菜单的path,比如detail页的crumbFrom就指向/order/list。面包屑生成时,如果当前路由meta有crumbFrom,就根据这个path去路由表里找到对应菜单项,拼出完整层级。这样不管从哪个列表跳进详情,面包屑都不会断层。
7. 完整数据流走一遍:从登录到页面的全链路
7.1 登录后到Layout渲染的完整调用链
前面的内容都是按组件和模块拆开讲的,这里把整条链路串起来走一遍,帮助你把骨架的运行机制形成一个整体画面。
用户打开系统,访问/login,登录成功后拿到token,前端把token存到本地和store里。随后触发路由守卫,发现没拉取过用户信息,调用getUserInfo请求,拿到当前用户的基本信息和角色编码。接着调用generateRoutes,传roles进路由生成逻辑,内部会根据权限过滤动态路由表,过滤出当前角色可访问的路由集合。每一条都通过router.addRoute挂载。挂载完成后,分支判断用户原本想访问的路径,如果是一个带query参数的路径,要完整保留fullPath再重新进入路由守卫,走一遍路由匹配,最终渲染Layout。
Layout渲染后,路由对应的组件会出现在内容区,侧边栏根据路由树生成菜单,顶栏根据当前路径生成面包屑,标签页store会记录当前tab并更新keep-alive缓存列表。整体闭环到这里完成。
7.2 权限指令与骨架的配合:v-permission指令是在Layout内完成校验的
页面骨架层面还有一个和RBAC强相关的小细节:按钮级别的权限指令。很多人会把v-permission和Layout混淆,但实际项目里,骨架搭建完之后,具体页面里的按钮是否显示,才是RBAC进入业务层面的最后一步。一个成熟的Layout,通常会把权限指令注册为全局指令,在模板里用v-permission="['order:detail:export']"来控制按钮显隐。
我项目当中是让每个页面在mounted阶段把页面自身需要的权限码上报到store,再在指令的mounted钩子里根据权限码判断显隐。这样做的优势是视图层不用写任何if判断,代码很干净,而且权限码集中管理,方便后续审计和统计。
8. 我在实际项目里踩过的Layout相关的坑
8.1 坑一:动态路由addRoute重复挂载导致页面混乱
这个坑出现的时机是用户在系统里频繁切换账号,第一次登录A角色,路由表里挂载了A角色的页面。退出后登录B角色,如果直接再遍历B角色路由表执行addRoute,A角色的一些路由其实还在路由表里。如果两个角色刚好有同名的路由name,新挂载的路由会覆盖旧的,页面上就会出现“B角色页面引用了A角色的组件”这种诡异现象。
解决办法是退出登录时重置路由。Vue Router 4里没有直接removeRoute全部路由的API,我这里的做法是:在定义动态路由之前,先把动态路由的name收集到一个数组里,退出登录时遍历这些name执行router.removeRoute(name),同时清空store里的路由表。重新登录后再根据新角色动态挂载,就不会串了。
8.2 坑二:从A页签切到B页签,B页面的缓存和滚动位置一团糟
标签页缓存除了include配置,还有一个常见问题是折叠面板和滚动位置。KeepAlive只保留了组件状态,但el-main的滚动条如果不在组件内部,而是在Layout层,那么切换tab后,滚动条位置不会自动复位,用户切到新页面发现还在上个页面的滚动位置,非常干扰操作。
我当时统一了方案:内容区滚动条下放给每个页面组件,让每个页面自己管自己的滚动位置。这样配合KeepAlive,切换后能恢复每个页面各自的滚动位置,体验正常很多。虽然每个页面都要加一个固定的样式容器,但为了体验,这点成本值得。
8.3 坑三:隐藏菜单的页面,侧边栏高亮和面包屑全乱了
这个前面其实已经提到过,detail页这类隐藏菜单场景,如果不做特殊处理,侧边栏高亮会失灵,面包屑也会只显示“订单详情”一个层级。实话说我项目里修这个bug还真的花了不少时间,最后发现关键是activeMenu和crumbFrom这两个meta字段设计。总结下来,菜单高亮和面包屑本质上是一套“路由归属关系”,必须在路由配置阶段就明确每个页面归属哪个菜单模块,没有这个映射,运行时靠猜测是没法稳定的。
8.4 坑四:折叠菜单动画卡顿和侧边栏宽度切换闪烁
这里不算技术难题,但很影响体验。侧边栏从220px折叠到64px时,如果el-menu的collapse动画和el-aside的width过渡同时触发,经常会出现菜单项内容和宽度不同步、闪烁的问题。调优的办法是把el-aside的宽度切换改成transition属性控制宽度,并且el-menu的collapse要等宽度过渡完成之后再触发,或者在二者之间加一个统一的动画时长,保证节奏一致。这个细节review的时候不仔细看真看不出来,但实际使用中非常明显。
9. 从零搭建Layout时推荐的落地步骤
如果你正准备开始搭建或者重构这套骨架,我建议按这个顺序推进,不要一上来就写代码。
第一步,整理路由配置规范。把目前所有页面梳理出来,分成静态路由和动态路由两块,静态路由只放登录页、404、重置密码这类系统级页面,动态路由全部走权限过滤。第二步,定义meta字段规范。title、icon、hidden、roles、activeMenu、crumbFrom、affix、canReuse,这些字段的意义和默认值先在文档里定清楚,这样后续的人写路由配置时不用猜。
第三步,编写Layout基础三件套。先不管权限,先把aside、header、main三个区域渲染出来,用写死的菜单数据验证组件结构,跑通“点击菜单跳路由,路由变化高亮菜单”的基础联动。第四步,接入动态路由。把写死的菜单数据换成动态生成逻辑,接上登录态和角色信息。第五步,做标签页和缓存。第六步,处理面包屑、滚动条、过渡动画这些体验细节。
我自己的项目就是按这个节奏做的,前四步速度很快,第五步第六步主要花时间在调细节上。遇到问题的时候,优先考虑自己的路由配置逻辑,而不是先去怀疑组件库有bug——我在排查过程中就有好几次以为是Element Plus的问题,最后发现还是自己的数据流哪个环节断了,这个经验供大家参考。