
做某内容平台项目进入第十天的时候我遇到一个绕不过去的坎。前九天页面还是平铺的一个路由对应一个页面URL 一配、组件一挂完事。但这一天要同时把用户端前台和管理员后台放进同一个项目老办法直接撑不住了。前台有导航、页脚、固定布局后台有侧边栏、顶栏、折叠菜单两边页面还要共用登录状态和权限体系。路由一下子从穿项链变成了叠罗汉我不得不正式搞起路由嵌套顺手把整套前后台架构重新搭了一遍。这篇就把 Day10 完整过程写出来。内容围绕两层一是嵌套路由的原理、配置、踩坑二是基于嵌套路由的前后台两套布局怎么落地。适合正在用 Vue 开发中后台项目、被路由结构和权限控制绕晕的人。看完你至少能分清静态路由表 动态嵌套路由 布局组件出口这三层关系也能避开我踩过的那几个坑。1. 为什么平铺路由撑不住真实项目路由嵌套要解决的问题1.1 我是在哪一刻决定必须用嵌套路由的Day10 的需求是给内容平台加管理后台。前台是用户浏览页后台是管理员操作区两者差异很大。前台页面长这样顶部导航、中间内容、底部版权三块结构几乎所有页面共用。后台页面则是左侧菜单、顶部面包屑、右侧内容区而且后台还需要二级菜单比如用户管理下面要挂用户列表和用户详情两个页面。如果用平铺路由硬写每个页面都要自己引入一遍导航组件。比如前台有 8 个页面我就得在 8 个页面的模板里都复制一份顶部导航代码改一处要同步改八处。后台更麻烦侧边栏的选中高亮逻辑写在哪菜单展开状态怎么在页面切换时保持一致这些问题在平铺结构下没有一个干净的答案。真正让我决定必须改架构的是 URL 的问题。后台用户详情的 URL 是/admin/users/detail/123从语义上看这个页面天然属于用户管理这个上级模块。但平铺路由里它就是一个独立的顶级路由和用户管理列表没有父子关系这导致后续做面包屑、做权限粒度控制、做菜单高亮全都别扭。我意识到路由不只是路径到组件的映射它本身就应该是项目结构的一种表达。1.2 嵌套路由的本质URL 层级、组件层级、布局复用三者合一嵌套路由的核心机制其实一句话就能说清父路由的组件负责渲染公共布局子路由的组件渲染在父组件内部的某个出口里。拿用户中心举例。我要实现这么一套结构进入用户中心的任何子页面顶部都是同一套个人资料卡和功能导航。用嵌套路由配置是这样的const routes [ { path: /user, component: UserCenterLayout, // 公共布局 children: [ { path: articles, component: UserArticles }, { path: settings, component: UserSettings } ] } ]访问/user/articles时渲染过程是这样的先渲染UserCenterLayout组件里有顶部导航、侧边导航、核心内容区等结构然后 Vue Router 会继续匹配到UserArticles把它渲染到UserCenterLayout模板里的router-view /位置。URL 的层级、组件的嵌套、布局的复用这三者在嵌套路由里是统一的。反过来想如果不用嵌套路由/user/articles和/user/settings就是两个平级路由各自组件都得包含完整的页头页脚。公共部分一旦修改所有页面都要跟着改维护成本非常高。所以嵌套路由解决的不只是好看它把公共部分的渲染提升到了父组件这是复用性和一致性的根本保证。1.3 嵌套路由对比平铺路由三个最直观的改善从 Day10 的实际对比看换成嵌套路由后有三个方面改善非常明显菜单高亮和面包屑有了数据来源。嵌套路由天然产生一条匹配链路route.matched数组里依次是父路由记录、子路由记录、孙路由记录。面包屑可以直接根据这条链路的meta.title生成侧边菜单也可以依据当前matched链判断哪个父菜单应该展开、哪个子菜单应该高亮。平铺路由阶段这些逻辑全靠手写判断现在变成了路由系统自带的信息。公共状态和动画可以收敛到布局层。前台和后台各自的布局组件可以在布局里统一处理滚动条恢复、页面进出场动画、keep-alive 缓存策略。子页面只管自己的业务内容互不干扰。平铺路由时每个页面都要重复做这些事情很容易出现有的页面处理了、有的页面没处理的零散现象。权限控制的粒度可以下沉。嵌套层级决定了权限控制的最小单元。在父路由上挂meta: { requiresAuth: true }它的所有子路由都受到这层约束。如果某个子路由想对权限做更细的限制自己再挂一层meta: { role: admin }就可以。这种层层叠加的模式比平铺路由里每个页面单独判断权限要清晰得多。2. 嵌套路由的配置要点从路由表到父组件出口2.1 基础语法children 里的 path 千万别带多余的斜杠嵌套路由的配置语法本身不难但有一个细节非常容易踩坑子路由的 path 不要以/开头。// 正确写法子路由 path 不带斜杠是相对父路由的路径 const routes [ { path: /admin, component: AdminLayout, children: [ { path: dashboard, component: AdminDashboard }, // 实际 URL: /admin/dashboard { path: users, component: AdminUsers } // 实际 URL: /admin/users ] } ]// 错误写法子路由 path 带了斜杠变成绝对路径 const routes [ { path: /admin, children: [ { path: /dashboard, component: AdminDashboard } // 这个路由和 /admin 没有层级关系 ] } ]一旦子路由的 path 带了/它就会被当成顶层路由处理。你访问/admin/dashboard会渲染AdminDashboard但AdminLayout不会被渲染公共布局直接就不见了子页面变成了一个孤儿页面。这是我 Day10 亲眼见过的错误一开始还以为是组件写错了排查了半小时才发现是路径多了个斜杠。这个规则记住就行了children 里的 path 一律不写斜杠除非你有意让它成为绝对路径。2.2 父组件必须留出口router-view 的位置决定布局结构嵌套路由配置好了但如果父组件模板里没有router-view /子路由永远渲染不出来页面就是一片空白。这是嵌套路由最容易犯、也最让人摸不着头脑的问题。父布局组件的模板结构大致是这样template div classadmin-layout aside classsidebar !-- 侧边菜单固定不变 -- /aside div classmain header classnavbar !-- 顶栏固定不变 -- /header main classcontent !-- 这里的 router-view 是子路由的渲染出口 -- router-view / /main /div /div /templaterouter-view /放在哪里子页面就渲染在哪里。想保留哪块公共区域就把出口放在那个区域的外面。比如后台布局里侧边栏和顶栏不想让子页面自己控制就放在出口外面内容区是每个页面都不一样的就对应的把出口放在内容区里。一个 layout 里其实也可以放多个router-view /这就是命名视图named view的用武之地。复杂后台布局中顶部、侧边、内容三块各有不同的渲染目标时可以给每个出口起一个名字父路由用components对象一次性声明{ path: /admin, components: { navbar: AdminNavbar, sidebar: AdminSidebar, default: AdminContent // 不写 name 的默认出口 } }不过以我的经验大多数项目一个默认出口就够了。多出口会让路由配置和组件映射变得隐晦除非确实有同一条 URL 下需要同时切换多个独立区块的需求否则先用一个出口简单直接。2.3 默认子页面怎么写redirect 与空路径两种方式的取舍访问/admin的时候你希望它自动展示哪个子页面这是嵌套路由里最常见的默认页需求。两种写法各有各的适用场景。第一种父路由配置redirect把访问/admin的动作重定向到某个确定的子路由{ path: /admin, component: AdminLayout, redirect: /admin/dashboard, // 访问 /admin 时自动跳转到 /admin/dashboard children: [ { path: dashboard, component: AdminDashboard }, { path: users, component: AdminUsers } ] }第二种给父路由配置一个path: 的空子路由让/admin本身直接渲染一个默认子组件{ path: /admin, component: AdminLayout, children: [ { path: , component: AdminDashboard }, // 访问 /admin 时直接渲染 AdminDashboard { path: users, component: AdminUsers } ] }两种方式的差别在于redirect 是URL 会变化访问/admin浏览器地址栏最终变成/admin/dashboard空路径是URL 不变地址栏一直停在/admin内容却是默认子页面。实际项目中我更推荐用 redirect。原因是 URL 一旦标准化到/admin/dashboard菜单高亮、面包屑、分享链接的语义都更清晰。空路径适合那种父路由本身就是一个有意义的页面不需要跳转的场景比如访问某个详情页根路径时直接展示摘要页。我自己一般只在特殊场景用空路径默认页统一用 redirect 搞定。2.4 命名路由和命名视图嵌套层级深了之后的安全带嵌套层数一多路径字符串就会变得又长又容易拼错。比如从 /admin/users/detail/123跳回/admin/users如果手动拼接路径中间任何一个路由改动都会牵连一片代码。这种场景下给每条路由加上name跳转全部走命名路由会安全很多{ path: users, name: AdminUsers, component: AdminUsers }, { path: users/detail/:id, name: AdminUserDetail, component: AdminUserDetail }跳转时这样写// 推荐按 name 跳转 router.push({ name: AdminUsers }) router.push({ name: AdminUserDetail, params: { id: 123 } })命名路由的价值在嵌套多层后越发明显。它让路由之间的逻辑关系体现在名字上而不是路径字符串上。后端改 URL 结构、前端拼写错误、链接路径失效这些风险都能降到最低。嵌套路由的层级一深字符串拼接跳转基本就是等着踩坑早点习惯用 name后面写权限控制时也会顺手很多。3. 前后台两套布局的落地路由、目录与页面文件的组织3.1 为什么前台后台要分成两条顶级路由分支前后台同处一个项目最忌讳的是把所有页面放在同一套布局里靠meta字段控制某块显示或不显示。短期看是省事了但两种角色的页面越来越多之后这套布局的模板会堆满各种条件判断侧边栏、顶栏、页脚的显示逻辑盘根错节改前台不敢动后台改后台怕影响前台。Day10 我采用了拆分方案前台和后台各占一个顶级路由分支各自配一套独立的 layout 组件。结构上看起来是这样/portal 前台PortalLayout 包裹 /admin 后台AdminLayout 包裹 /login 登录页独立页面不归属任何布局前台分支里可以继续嵌套用户中心等子结构后台分支里按模块再往下挂。两条分支互不干扰前台再改导航、后台再叠菜单都不会串线。后面做权限控制也轻松直接在/admin这层父路由上挂requiresAuth整条后台分支天然受保护不需要逐个页面配置。有的项目还会有用户中心这类介于前台和后台之间的结构我的处理是把它当前台下的一个子布局而不是单独开一条顶级分支。因为它的整体外壳还是前台风格只是内部有自己的二级导航。这正好是嵌套路由的经典应用每一层嵌套本质是在当前的页面风格下面再划分一套局部的公共部分。3.2 完整路由表示例登录页、前台分支、后台分支、404 兜底下面是我 Day10 最终落地的路由表结构去掉业务细节留核心骨架const routes [ { path: /login, name: Login, component: () import(/views/login/LoginPage.vue) }, { path: /portal, component: () import(/layout/PortalLayout.vue), children: [ { path: , redirect: /portal/home }, { path: home, name: PortalHome, component: () import(/views/portal/HomePage.vue) }, { path: user, component: () import(/views/portal/user/UserCenterLayout.vue), children: [ { path: , redirect: /portal/user/articles }, { path: articles, name: PortalUserArticles, component: () import(/views/portal/user/MyArticles.vue) }, { path: settings, name: PortalUserSettings, component: () import(/views/portal/user/UserSettings.vue) } ] } ] }, { path: /admin, component: () import(/layout/AdminLayout.vue), meta: { requiresAuth: true, role: admin }, children: [ { path: , redirect: /admin/dashboard }, { path: dashboard, name: AdminDashboard, component: () import(/views/admin/DashboardPage.vue) }, { path: users, name: AdminUsers, component: () import(/views/admin/user/ManageUsers.vue) }, { path: users/detail/:id, name: AdminUserDetail, component: () import(/views/admin/user/UserDetail.vue) } ] }, { path: /:pathMatch(.*)*, name: NotFound, component: () import(/views/error/NotFoundPage.vue) } ]几个设计点说明一下。第一所有页面组件都用动态导入/() import(...)这样 Vue Router 会把每个页面拆成独立 chunk首屏只加载登录页和当前分支的代码后台代码不会打进前台用户的下载包里。第二两个分支的父路由都配置了redirect: 让根路径有默认归属。前台落到/portal/home后台落到/admin/dashboard访问不带子路径的根地址时用户不会面对空白页。第三404 兜底路由放在最后用/:pathMatch(.*)*捕获所有未匹配的路径。这里要注意通配路由必须放最后否则它会抢先拦截所有合法路由。嵌套路由的层级越深通配路由的位置就越要谨慎放在最后是铁律。3.3 目录结构设计与路由懒加载分包路由表的组织方式直接反映项目结构。我按页面归属把views目录拆成三大块路由文件也单独分模块src/ ├── layout/ │ ├── PortalLayout.vue # 前台布局 │ └── AdminLayout.vue # 后台布局 ├── router/ │ ├── index.js # createRouter 路由汇总 │ ├── portalRoutes.js # 前台路由模块 │ └── adminRoutes.js # 后台路由模块 └── views/ ├── login/ ├── portal/ │ └── user/ └── admin/ └── user/路由模块化的核心目的不是少写代码而是让哪个页面归哪个模块管一目了然。我后来动态加权限路由时就是直接读取adminRoutes.js里导出的数组筛选出当前角色有权限的项再注册进去不需要再翻目录找页面。配套懒加载后打包产物大致是这样的结构PortalLayout和前台页面相关代码一个 chunkAdminLayout和后台业务代码一个 chunk登录页单独一个 chunk。访问前台时后台代码完全不加载反过来也一样。嵌套路由一定要配合懒加载使用否则嵌套层级带来的结构调整都会被全量打包抵消掉白费功夫。3.4 嵌套路由推荐的跳转方式嵌套层级多了以后跳转时最容易出问题的就是相对路径。我见过有人写router-link toarticles想着从/portal/user相对跳到/portal/user/articles但 Vue Router 对相对路径的解析规则相对容易产生歧义一旦路径嵌套层级变化链接就悄悄失效了。我的建议很明确嵌套路由跳转一律使用绝对路径或命名路由不要依赖相对路径。!-- 推荐命名路由 -- router-link :to{ name: PortalUserArticles }我的文章/router-link !-- 推荐绝对路径 -- router-link to/portal/user/articles我的文章/router-link !-- 不推荐相对路径解析规则容易产生歧义 -- router-link toarticles我的文章/router-link命名路由的好处在于路径怎么改都不影响代码。比如后来我把/admin/users/detail/:id调整成/admin/user/detail/:id所有引用AdminUserDetail的跳转代码一行都不用动这种维护成本的优势在嵌套层级变深后会非常明显。4. 嵌套路由实战中我踩得最深的那几个坑4.1 页面空白父组件缺 router-view 的完整排查链路说到嵌套路由第一个坑就是页面空白。我 Day10 第一次配嵌套路由时就遇到了路由表写了、子组件也建了运行起来访问子路由 URL 正常变化但页面就是一片空白。完整的排查思路是这样走的先打开浏览器开发者工具看 DOM 结构。发现父布局组件渲染出来了但内容区是空的。这就说明问题不在路由匹配而在子组件没有渲染出来。再检查父组件的模板果然我只写了侧边栏和顶栏内容区完全没有router-view /。子路由匹配到了但没有出口可以渲染。如果父组件已经写了router-view /还空白就继续检查 keep-alive 的问题。当父组件被keep-alive包裹时出口通常要配合动态组件的方式写router-view v-slot{ Component } keep-alive component :isComponent / /keep-alive /router-view直接写keep-aliverouter-view //keep-alive在 Vue 3 里其实是优先级的问题不保证符合预期。改写成v-slot 动态组件方式后子路由的渲染就正常了。排查之后总结一下这个坑的本质是路由匹配只解决组件该不该渲染的问题但渲染到哪里由父组件的模板结构决定。这两个环节脱节时页面看起来就是有 URL 没内容。4.2 子路由悄悄变成了顶层路由path 写法的坑第二个坑我在第 2 章提过但 Day10 里它造成的问题更隐蔽。当时后台有个表单页我想让它和列表页共用一个布局写配置的时候手一抖children 里的 path 写成了/form{ path: /admin, component: AdminLayout, children: [ { path: users, component: AdminUsers }, { path: /form, component: AdminForm } // 这里多了斜杠 ] }现象是访问/admin/form时页面渲染了但布局完全不对侧边栏、顶栏全都不见了只剩一个孤零零的表单在浏览器最左上角。当时第一反应是布局组件出问题了调了半天没找到原因。后来我把route.matched打印出来才看到匹配到的路由记录只有一条AdminLayout根本不在这条匹配链路里。问题一下子就清楚了子路由 path 带了斜杠它被当成了独立的顶层路由不再嵌套在/admin下面。记这个教训很简单children 里的 path只有两种情况——要么不写斜杠作为相对路径要么确实想把它变成顶层路由就移出去放到 routes 顶层。两者取一个不要在 children 里用斜杠开头的路径十有八九是误操作。4.3 redirect 把自己绕进死循环的现场第三个坑是 redirect 配错了。当时我想让访问/admin默认跳到/admin/users但写的时候不小心写成了重定向到父路由自身{ path: /admin, redirect: /admin, // 自己重定向自己死循环 children: [...] }运行时页面直接报错Maximum call stack size exceeded或者无限重定向。浏览器地址栏疯狂闪烁页面一直跳转不停。原因很好理解访问/admin要跳转到/admin跳过去之后又要跳转到/admin永远有个不停的循环。同类坑还有一种redirect 写到一个不存在的子路径比如redirect: /admin/userxxx结果跳过去之后匹配不到任何路由又被 404 兜底捕获表现是页面空白或者直接跳到 404。所以每次配 redirect 都检查一下目标路径是否真的存在于该分支的 children 里。父子路由之间不要互相 redirect这是铁律。4.4 动态添加子路由后嵌套关系失效addRoute 的挂载点权限系统里经常要根据用户角色动态添加路由。Day10 做后台权限时我踩了一个隐蔽的坑用router.addRoute添加后台子路由时如果只传路由对象不传父路由的 name它会被加到顶层去嵌套关系直接失效。// 错误添加到顶层嵌套关系失效 router.addRoute({ path: users, component: () import(/views/admin/user/ManageUsers.vue) }) // 正确第一个参数传父路由的 name挂到 /admin 分支下 router.addRoute(AdminLayout, { path: users, name: AdminUsers, component: () import(/views/admin/user/ManageUsers.vue) })addRoute的第一个参数就是父路由的 name不传的话新路由就是顶层路由和/admin没有任何父子关系布局、权限、面包屑全部失效。这个参数极易被忽略因为编译器不检查运行时不报错只有等你看到页面布局不对了才会发现。4.5 keep-alive 和嵌套路由缓存错乱的组合嵌套路由配合 keep-alive 是个大坑尤其是缓存颗粒度的问题。后台内容区做了 keep-alive 之后我只想缓存列表页让它翻页后回来不丢失状态。但实际运行时发现连详情页也被缓存了从用户 A 的详情切到用户 B 的详情页面内容还是用户 A 的。问题出在component :isComponent /的key设置。如果不加 key同一路由组件切换时会复用上一个实例参数变了组件却不重新创建。加上:key$route.fullPath后每个不同路径都会有独立实例缓存粒度就正常了router-view v-slot{ Component } keep-alive :includecachedViews component :isComponent :key$route.fullPath / /keep-alive /router-view但是这里有个取舍$route.fullPath作为 key意味着同一个组件、不同 query 参数会创建新实例这有时候也是我们想要的比如详情页不同 id 确实是不同内容。如果某个组件你希望所有参数都复用同一个实例可以把 key 改为$route.name。用什么 key取决于业务希望组件的生命周期跟谁走。在嵌套路由层级较深时这个决定最好趁早统一不然后期每个页面的缓存行为都会不一样查起来很痛苦。5. 权限联动下的动态路由前后台权限控制的整合方案5.1 哪些路由静态哪些路由动态前后台架构搭好后权限控制是绕不开的。Day10 的思路是把路由分成两类静态路由是所有人都能访问的/login、/portal下的前台页面、404 兜底。这些路由在应用启动时就注册好不需要任何权限判断。动态路由是登录后根据角色才能注册的/admin分支下的所有业务页面。管理员能看用户管理、内容审核普通运营人员可能只看内容审核看不到用户管理。这些路由在应用启动时不能注册必须等登录接口返回角色权限之后再把当前角色有权限的路由动态添加进去。静态路由和动态路由分开管理的最大好处是权限系统的行为是可预测的。没有权限的路由在应用里根本不存在访问它直接落到 404比路由存在但跳转提示无权限要干净得多。5.2 beforeEach 全局守卫的完整执行逻辑权限控制的枢纽是全局前置守卫router.beforeEach。它在每次路由跳转前执行我在这里完成三件事登录状态检查、动态路由加载、目标路由放行。router.beforeEach(async (to, from, next) { const token useAuthStore().token // 1. 未登录且非登录页跳去登录 if (!token to.path ! /login) { return next({ path: /login, query: { redirect: to.fullPath } }) } // 2. 已登录且访问登录页跳去首页 if (token to.path /login) { return next({ path: /portal/home }) } // 3. 已经登录但动态路由还没加载先加载再重新进入目标 if (token !usePermissionStore().routesLoaded) { const accessRoutes await usePermissionStore().generateRoutes() accessRoutes.forEach(route router.addRoute(route)) return next({ ...to, replace: true }) } next() })这个逻辑最关键的部分是第三步的return next({ ...to, replace: true })。当时我看很多教程只写到addRoute 之后直接 next()结果一运行就是死循环页面疯狂刷新。根因是addRoute 是异步生效的守卫里 addRoute 执行完当前这次跳转的匹配结果可能还是旧的to目标路由依然被认为是未匹配的。直接放行的话守卫下一次触发时依然走routesLoaded 为 false的分支于是无限循环。加上{ ...to, replace: true }后守卫会带着当前目标路由的完整信息重新发起一次跳转。这次跳转时动态路由已经注册完成routesLoaded也已经是 true走正常分支就通过了。replace: true是为了不让这次重进在历史记录里多留一层用户按后退按钮时不会莫名其妙退回一页。5.3 动态路由刷新丢失恢复方案的取舍动态路由有个绕不开的问题页面一刷新内存里的动态路由全部丢失。因为router.addRoute添加的路由只活在当前应用实例的内存里刷新后应用重启路由表回到初始的静态状态。此时用户直接访问/admin/users匹配不到路由就会落进 404。解决办法有三个层级。最省事的方案是在守卫里发现目标是 /admin 下的路径但当前路由表里没有匹配时先判断本地有没有存储权限标识如果有就重新加载动态路由再放行。核心逻辑还是 5.2 那段代码只是routesLoaded不能只看内存要结合持久化的状态判断。比较主流的做法是把角色信息存在本地存储里刷新后通过角色重新请求权限数据、重新addRoute。接口返回快用户几乎感知不到刷新过程中路由重新注册的过程。如果你的权限接口较慢可以在守卫第三步加一个等待路由加载完成的状态标记防止用户刷新后立刻点击页面导致匹配失败。这个方案我建议直接落地不要在刷新恢复上做太多花活。权限系统的核心要求是不泄露、不崩溃、可恢复不泄露在于路由不能提前注册不崩溃在于守卫逻辑不能死循环可恢复在于刷新后权限数据能重新拉起来。6. 路由的工程化收尾meta 规划、面包屑、标题与滚动行为6.1 把 meta 当作路由的身份证嵌套路由层级深了之后meta字段的重要性会急速上升。它相当于路由的身份证所有展示层需要的信息都可以提前挂在上面而不是在组件里临时判断。我 Day10 给每条路由都规划了统一的 meta 结构{ path: users, name: AdminUsers, component: () import(/views/admin/user/ManageUsers.vue), meta: { title: 用户管理, icon: user, requiresAuth: true, role: admin } }title给面包屑和浏览器标签页标题用icon给侧边菜单渲染用requiresAuth给导航守卫判断登录态用role给粒度更细的角色权限用统一 meta 结构这件事我当时一开始没做导致后面面包屑和菜单只能到处硬编码。后来花了半天把所有路由的 meta 补齐后面所有展示组件都只需要读route.meta就够了。建议你在建路由表的第一天就把 meta 约定好别等页面多了再补代价是逐条修改路由记录。6.2 面包屑和菜单高亮route.matched 一次搞定嵌套路由的天然优势是route.matched会按层级返回一组路由记录。比如访问/portal/user/articles匹配链路是PortalLayout → UserCenterLayout → UserArticles。只要每条路由都有 meta.title面包屑就是一次简单的转换const breadcrumbs route.matched .filter(item item.meta item.meta.title) .map(item ({ title: item.meta.title, path: item.path }))如果某层的路由不想出现在面包屑里就把meta.title留空或者加一个meta.breadcrumb: false标记生成时过滤掉即可。菜单高亮同理。侧边菜单根据当前route.matched里是否有某个菜单对应的路由记录来判断展开和高亮状态。比每个页面手动设置 activeMenu要优雅得多而且嵌套层级怎么加深高亮逻辑都不用改。这里有个细节要提醒父布局组件的 path 可能是/admin或/portal这类布局路由通常没有业务标题如果不做过滤面包屑第一级会显示一个空 title 或者奇怪的占位。记得在 meta 里给布局路由设计合适的 title或者干脆在生成面包屑时跳过没有 title 的中间层。6.3 动态标题和页面滚动两个不起眼但影响体验的点最后说两个小优化虽然不起眼但直接影响使用体验。一个是浏览器标签页标题。嵌套路由切换时我建议把 document.title 和当前路由的 meta 联动起来而不是每个页面组件自己写document.title ...。全局守卫里统一处理router.afterEach((to) { const titles to.matched.filter(item item.meta item.meta.title) const baseTitle 某内容平台 if (titles.length) { document.title ${titles[titles.length - 1].meta.title} - ${baseTitle} } else { document.title baseTitle } })to.matched最后一项通常是具体的页面路由取它的 title 最为准确。层级越深标题就应该是具体页面 父模块 平台名这种由近及远的组合。另一个是页面滚动行为。嵌套路由默认情况下切换到新页面时浏览器滚动位置不会自动复位你会停在上一个页面的滚动位置。在创建路由时配置 scrollBehaviorconst router createRouter({ history: createWebHashHistory(), routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition // 后退时恢复原位置 } return { top: 0 } // 新页面回到顶部 } })不过注意如果后台内容区是自定义滚动容器比如.content区域 overflow: auto路由的 scrollBehavior 管不到这个容器需要在布局组件里监听路由变化手动把容器滚回顶部。这个坑我先说在前面scrollBehavior 只负责浏览器窗口的滚动自定义滚动容器要自己处理别指望路由系统一把梭。收个尾说说 Day10 之后的体会。路由嵌套这套东西语法本身不难难的是把URL 层级、组件嵌套、布局复用、权限控制这四个维度在脑子里统一成一张图。只要你脑子里有这张图后面加页面、加模块、加权限都是一层一层往上叠的事。最后再分享一个我从 Day10 之后一直沿用的习惯每新建一条路由第一件事就写 meta 的 title 和 requiresAuth别等做完页面再补。这个习惯帮我省掉了后面无数个面包屑缺标题菜单高亮不对刷新后路由丢失的调试时间。路由表是骨架meta 是骨架上的关节一开始就把关节接好整个项目才能立得住。