Vue3动态路由实战:菜单接口、约定式与权限控制的完整实践
2026/9/18 11:17:14 网站建设 项目流程

做过 Vue 后台管理系统的人,应该都经历过这种尴尬:侧边栏菜单是写死写在路由配置里的,产品临时提了个新页面,你要去 router 文件里手动 import 一个组件,再配一条 route,有时候还得顺便给菜单文件加一项。一次两个页面还好,项目过了几十个页面之后,这种玩法基本是灾难。后来接了个权限管理需求,要求不同角色登录后看到的菜单完全不一样,我才正式把动态路由这套机制啃下来。

在 Vue3 + Vite 项目里,动态路由这个话题被聊得非常散,网上随便一搜就是"动态路由实现""addRoute 用法",但真正落到自己项目里,你会发现需求之间的差别非常大。我这几年折腾下来,"动态路由"真正高频出现的也就三种场景:后端菜单接口驱动的路由表、基于 Viteimport.meta.glob的文件约定式路由、基于用户角色权限的守卫挂载与动态过滤。这篇文章不整虚的,把每个场景的适用环境、核心代码、以及我踩过的坑全部拆开讲。

1. 动态路由的三种需求模型:先搞明白你要的是哪一种

很多人在网上问"动态路由怎么实现"时,其实自己也没想清楚要解决什么问题。这里先帮大家把概念理清:所谓"动态路由",关键在一个"动"字,但动态的方向可以完全不同。

第一种是数据驱动的路由表。路由的配置信息,比如 path、name、meta、children,不再是前端写死的,而是由后端接口统一返回。前端收到菜单树之后,把它转换成 vue-router 能识别的路由记录,然后通过router.addRoute动态注册。这种场景多出现在后台管理系统,尤其是需要运营人员在后台配置菜单、调整页面入口的项目。它的典型特征是你登录系统后拿到的菜单,跟其他人的菜单可能完全不一样,但这个差异体现在路由的"内容"上。

第二种是约定式路由。路由结构完全由 views 目录下的文件路径决定,新增页面时只需要在指定目录下新建一个.vue文件,路由自动就出现了,不用再去路由配置文件里手动维护。这种方案的底气来自 Vite 提供的import.meta.glob能力,在 webpack 时代你还得自己写 require.context,Vite 这里写起来简洁得多。它适合页面数量多、团队约定意识强的项目,尤其是原型阶段和内部系统。

第三种是权限控制下的路由挂载。不管路由表是静态配置还是后端接口下发,前端都需要根据当前登录用户的角色或权限码,决定哪些路由可以注册、哪些路由不能访问。这种场景一般配合beforeEach导航守卫完成:用户刷新页面后,先判断登录态,再拉取权限、过滤路由表、动态挂载,最后放行导航。

一个现实项目往往不是单一场景。我见过大多数商用后台都是"静态路由打底 + 接口菜单动态路由 + 递归映射组件 + 守卫拦截"的组合打法,文件约定式作为组件加载的技术支撑,角色权限作为过滤逻辑穿插其中。后面我会把每个场景单独拆开,再讲怎么在同一个项目里把它们缝合起来。

2. 场景一:后端菜单接口驱动,前端递归生成路由记录

这个场景是后台管理系统里最典型的动态路由方案,市面上主流的中后台框架,包括若依这类开源项目,核心逻辑都差不多。

2.1 后端返回的菜单数据长什么样

先说约定。动态路由不是把整个 vue-router 配置文件搬到后端,而是让后端返回一份"菜单树",前端拿到后转换成路由记录。我们项目里后端返回的数据大概是下面这个结构:

[ { "path": "/dashboard", "name": "Dashboard", "component": "dashboard/index", "meta": { "title": "工作台", "icon": "dashboard", "affix": true } }, { "path": "/system", "name": "System", "component": "Layout", "meta": { "title": "系统管理", "icon": "setting" }, "children": [ { "path": "/system/user", "name": "SystemUser", "component": "system/user/index", "meta": { "title": "用户管理", "icon": "user" } }, { "path": "/system/role", "name": "SystemRole", "component": "system/role/index", "meta": { "title": "角色管理", "icon": "role" } } ] } ]

有几个字段需要提前约定清楚。path是访问路径,name是路由名,component是前端组件的映射标识,这个字段后端存的是字符串,绝对不能直接拿来做动态 import。meta里主要放标题、图标、缓存标记这些页面辅助信息。为什么 component 用字符串而不是真实组件路径?很简单,后端不可能知道你的前端组件存在哪个目录下,也不可能返回一个函数给你,所以必须约定一套组件标识规则,由前端来做字符串到组件的解析。

2.2 核心实现:把菜单树递归转换成路由记录

拿到菜单数据后,第一件事是转换成 vue-router 需要的RouteRecordRaw。这个过程必须递归处理,因为菜单是树形的,路由也天然应该有 children 嵌套关系。

import type { RouteRecordRaw } from 'vue-router' import Layout from '@/layout/index.vue' interface MenuData { path: string name: string component: string meta?: Record<string, any> children?: MenuData[] } const viewModules = import.meta.glob('/src/views/**/*.vue') function resolveComponent(component: string): any { if (component === 'Layout') { return Layout } // 约定 component 字符串对应 src/views 下的文件路径 const target = Object.keys(viewModules).find( (key) => key === `/src/views/${component}.vue` ) if (!target) { console.warn(`[DynamicRoute] 未找到组件: ${component}`) return () => import('@/views/error/404.vue') } return viewModules[target] } function transformMenuToRoutes(menus: MenuData[]): RouteRecordRaw[] { return menus.map((menu) => { const route: RouteRecordRaw = { path: menu.path, name: menu.name, meta: menu.meta ?? {}, } if (menu.component) { route.component = resolveComponent(menu.component) } if (menu.children && menu.children.length) { route.children = transformMenuToRoutes(menu.children) } return route }) } export async function generateDynamicRoutes(menus: MenuData[]) { return transformMenuToRoutes(menus) }

注意这里我用了import.meta.glob('/src/views/**/*.vue')预加载所有页面组件。viewModules这个对象,key 是文件路径,value 是一个返回 Promise 组件的懒加载函数。resolveComponent就是根据后端返回的字符串,在这个映射表里找到对应的加载函数。这套做法的好处是,以后新增页面只需要在 views 目录下加文件,不用再维护一套额外的组件映射表。至于为什么 component 是字符串而不是真实的路径,核心考虑是安全性和可维护性:后端返回一个受控的标识符,前端严格映射,避免把文件路径直接暴露到接口层。

2.3 路由注册与刷新防白屏

路由表转换完了,接下来的问题是"什么时候 addRoute"以及"刷新页面怎么办"。直接在登录页拿到菜单后就添加,这在单页应用里看着没问题,但刷新一次就全没了。因为刷新之后内存里的 Vuex/Pinia store 被重置,路由表是空的,所以要有一个"初始化一次"的动作。

// store/modules/permission.ts import { defineStore } from 'pinia' import { generateDynamicRoutes } from '@/router/dynamic' export const usePermissionStore = defineStore('permission', { state: () => ({ isDynamicRoutesAdded: false, dynamicRoutes: [] as RouteRecordRaw[], }), actions: { async buildDynamicRoutes(menus: MenuData[]) { const routes = await generateDynamicRoutes(menus) this.dynamicRoutes = routes this.isDynamicRoutesAdded = true return routes }, reset() { this.dynamicRoutes = [] this.isDynamicRoutesAdded = false } } })

守卫里这样写:

// router/index.ts import { usePermissionStore } from '@/store/modules/permission' import { useUserStore } from '@/store/modules/user' const whiteList = ['/login'] router.beforeEach(async (to) => { const userStore = useUserStore() const permissionStore = usePermissionStore() if (!userStore.token) { if (whiteList.includes(to.path)) return true return `/login?redirect=${encodeURIComponent(to.fullPath)}` } if (userStore.token && to.path === '/login') { return '/' } // 关键:动态路由只初始化一次 if (!permissionStore.isDynamicRoutesAdded) { try { // 用户信息在登录时已经存进 store,这里直接取 const menus = userStore.menus const routes = await permissionStore.buildDynamicRoutes(menus) routes.forEach((route) => router.addRoute(route)) // 重新进入一次当前导航,让新添加的路由生效 return { ...to, replace: true } } catch (error) { // 拉取菜单失败,清空登录态回登录页 await userStore.resetToken() return `/login?redirect=${encodeURIComponent(to.fullPath)}` } } return true })

这里有一个非常容易踩的坑:拿到动态路由之后,第一次导航的to对象其实已经匹配完了,如果当前访问的是刚加进去的路由,直接return true是不会正确渲染的。必须return { ...to, replace: true }强制触发一次新的导航,让路由匹配结果刷新。我第一次写这块逻辑时漏了这一步,结果登录后跳转首页正常,一刷新首页就白屏,排查了半天才发现是这个问题。

2.4 为什么 404 路由总是拦在最前面

动态路由另一个高频坑是:明明 addRoute 成功,访问/system/user却落进了 404 页面。原因在于通配路由/:pathMatch(.*)*注册得太早。

如果你在createRouter的 routes 数组里就写死了 404 兜底路由,那么当用户刷新/system/user页面时,路由匹配是从已有路由表里找的。此时动态路由还没添加,通配路由就接管了。等你的动态路由 addRoute 进去,一切已经晚了。

解决办法有三种:第一,404 路由不要放在静态路由表里,等动态路由全部添加完后再router.addRoute({ path: '/:pathMatch(.*)*', component: NotFound });第二,通配路由保留,但守卫里判断如果是授权之外且动态路由未初始化时,先执行完动态添加逻辑再进入导航;第三,404 页面的 render 逻辑做特殊处理,匹配不到的时候重新跳一次。我个人的习惯是方案一,最干净。动态路由添加之后,菜单和路由始终是一份完整数据,404 永远只在最后兜底。

3. 场景二:Vite 的 import.meta.glob,让路由跟着目录结构走

第二种场景和接口没什么关系,它是 Vite 项目独有的便利特性:路由表根据 views 目录下的文件名自动生成。团队只要约定好规则,新增页面 = 新建文件,不用维护任何路由配置。

3.1 import.meta.glob 到底做了什么

import.meta.glob是 Vite 提供的一个特殊语法,它接收一个静态字符串路径,返回一个对象。下面这行代码是场景二的核心:

const pages = import.meta.glob('/src/views/**/*.vue')

pages 的值大概是这样的:

{ '/src/views/dashboard/index.vue': () => import('/src/views/dashboard/index.vue'), '/src/views/system/user/index.vue': () => import('/src/views/system/user/index.vue'), '/src/views/error/404.vue': () => import('/src/views/error/404.vue') }

注意两个细节。第一,glob 参数必须是一个静态字符串,不能用变量拼,Vite 是在编译阶段解析它的;第二,默认返回的是懒加载函数,真正点击路由时才去请求组件代码,正好契合路由懒加载的需求。还有一个可选参数{ eager: true },作用是把默认值直接变成组件对象而不是函数,适合不需要懒加载的场景。

我在实际项目里还遇到过 glob 路径写错的案例:直接用import.meta.glob('src/views/**/*.vue'),少了最前面的/,编译时 Vite 会直接把整个字符串作为相对路径,匹配结果为空。这个坑虽然提示明显,但新手见到pages是个空对象时往往会一脸懵。

3.2 从文件路径推导出路由 path

文件路径是绝对路径,第一步要把它转换成路由地址。我常用的约定是:

  • /src/views/dashboard/index.vue对应路由/dashboard
  • /src/views/system/user/index.vue对应路由/system/user
  • /src/views/error/404.vue对应路由/error/404

处理逻辑很简单,剥掉/src/views前缀,去掉.vue后缀,再把/index替换成空。完整代码如下:

function filePathToRoutePath(filePath: string): string { return filePath .replace('/src/views', '') .replace(/\.vue$/, '') .replace(/\/index$/, '') || '/' } function generateRoutes() { return Object.keys(pages).map((filePath) => { const routePath = filePathToRoutePath(filePath) return { path: routePath, name: routePathToName(routePath), component: pages[filePath], } }) }

name 的生成建议也用 routePath 推导,比如把/system/user转成驼峰SystemUser,这样保证唯一性,同时也能配合keep-alive组件缓存和router.removeRoute

3.3 嵌套路由与 Layout 的处理

文件路径推导路由,最麻烦的是嵌套关系。如果系统页面都挂在同一个后台布局组件下,顶层一定需要一个 Layout 容器。直接遍历生成的扁平路由是行不通的,因为 Layout 需要套在所有业务页面外面。

我的做法是:生成路由时先按顶层目录分组,每个顶层目录作为一级路由,下面的文件作为它的 children。比如/src/views/system/user/index.vue会生成{ path: '/system', component: Layout, children: [{ path: 'user', component: ... }] },这样/system/user就正确渲染在布局内。

function buildRoutesWithLayout(filePath: string, page: any): RouteRecordRaw { const segments = filePath .replace('/src/views', '') .replace(/\.vue$/, '') .split('/') .filter(Boolean) if (segments.length === 0) { return { path: '/', component: page } } if (segments.length === 1) { return { path: `/${segments[0]}`, component: page } } const parentPath = `/${segments[0]}` const childRelativePath = segments.slice(1).join('/').replace(/^index$/, '') return { path: parentPath, component: Layout, children: [ { path: childRelativePath || '', component: page, }, ], } }

不过说实话,当你的路由嵌套超过两层,同时还涉及动态参数、重定向、命名视图时,约定式路由的代码复杂度会快速上升,维护成本不一定比手写路由低。所以场景二我建议用在页面结构相对规整、层级固定的中后台项目上。

3.4 约定式路由的边界条件

场景二看着省事,但它有几个明显的边界问题。第一个是动态参数路由,比如详情页/article/:id,目录结构怎么表达?常见做法是约定_id.vue[id].vue表示动态段,然后在生成 path 时做一次转换。第二个是重定向,比如/需要跳转到/dashboard,约定式路由没法自动表达,只能靠额外配置或手动补一条。第三个是命名路由冲突,文件名重复会导致 path 重复,Vite 编译期不会报错,但运行时页面会互相覆盖。

我自己在使用场景二时,一般会搭配一个page.config.js或单独的路由配置文件来做"例外处理",约定式路由负责 90% 的常规页面,剩下的靠白名单配置补齐。这样既享受了自动化的便利,又不至于被约定限制死。

4. 场景三:基于角色权限的守卫挂载,控制谁能看到哪个路由

如果说场景一和场景二是路由的"生产和组织结构",场景三就是路由的"访问控制"。这也是权限系统的最后一个环节,解决的是另外一个问题:用户没有某个路由的权限时,直接输入 URL 访问怎么办。

4.1 权限模型先定好:角色、权限码、路由表如何关联

做动态路由权限之前,先想清楚权限模型。最基础的是 RBAC 模型:用户属于角色,角色拥有菜单/权限码,路由表带权限标签。具体到 Vue 项目里,我通常给每个路由的meta添加一个permissions字段,存一个权限码数组:

{ path: '/system/user', name: 'SystemUser', component: () => import('@/views/system/user/index.vue'), meta: { title: '用户管理', permissions: ['system:user:list'] } }

登录之后,后端返回当前用户拥有的权限码列表,比如['system:user:list', 'system:role:list']。前端的任务就是检查权限码,再过滤动态路由表。这个方案比在 meta 里写 roles 数组更细,因为 roles 是粗粒度的,两个角色可能同时要访问同一个菜单,但按钮级的操作权限完全不同。

4.2 路由过滤函数:递归过滤静态路由表

如果动态路由的来源本身就是一组静态配置(比如内部系统菜单固定,只是不同角色可见性不同),那就不需要走接口菜单那一套,直接在守卫里过滤即可:

function hasPermission(permissions: string[], routePermissions?: string[]): boolean { if (!routePermissions || routePermissions.length === 0) return true return routePermissions.some((p) => permissions.includes(p)) } function filterRoutesByPermission( routes: RouteRecordRaw[], permissions: string[] ): RouteRecordRaw[] { return routes .filter((route) => hasPermission(permissions, route.meta?.permissions)) .map((route) => { if (route.children && route.children.length) { return { ...route, children: filterRoutesByPermission(route.children, permissions), } } return route }) }

这里有一个关键细节:meta.permissions不存在的路由,也就是没有权限要求的路由,默认放行。比如登录页、首页这种基础路由,不应该因为某次过滤被误杀。还有,过滤完 children 之后,父级如果只剩一个空壳,没有 children 也没有 component,这个父级路由就不应该再保留,否则会出现一个没有内容的空白菜单。

4.3 守卫中动态挂载的完整流程

权限守卫和场景一里的刷新防白屏逻辑是连在一起的。完整流程应该是:刷新页面 -> 守卫判断 token -> 拿到用户信息和权限码 -> 检查是否已经挂载过动态路由 -> 没挂载就过滤并 addRoute -> 重新导航。

router.beforeEach(async (to) => { const userStore = useUserStore() const permissionStore = usePermissionStore() if (!userStore.token) { if (whiteList.includes(to.path)) return true return `/login?redirect=${encodeURIComponent(to.fullPath)}` } if (!permissionStore.isDynamicRoutesAdded) { const permissions = userStore.permissions const allRoutes = await fetchRoutes() const accessibleRoutes = filterRoutesByPermission(allRoutes, permissions) accessibleRoutes.forEach((route) => { router.addRoute(route) }) permissionStore.isDynamicRoutesAdded = true return { ...to, replace: true } } // 已经没有权限访问时,落到 404 return true })

代码看着简单,但有几个细节必须注意。第一,fetchRoutes可能走的是接口,也可能直接就是从静态模块里取的,看你选用场景一还是直接用静态路由表。第二,isDynamicRoutesAdded这个标志位一定不能只是随便一个布尔值,要放进 store,因为守卫是全局的,刷新后需要重新初始化。第三,登出时一定要调用permissionStore.reset(),否则下次登录其他账号时,上一个账号的路由还在路由表里,会造成权限泄露。

4.4 动态路由的重复添加与清理

vue-router 4 的addRoute方法设计得比较灵活,重复添加同名路由时,默认会警告但不报错。如果你在登出时没有 removeRoute,下一次登录再 addRoute,很容易出现"路由重复定义,页面渲染异常"的诡异 bug。

保险的做法是,添加之前先判断:

if (!router.hasRoute(route.name)) { router.addRoute(route) } else { router.removeRoute(route.name) router.addRoute(route) }

登出时的清理动作也要做全:

function resetRouter() { permissionStore.getDynamicRoutes.forEach((route) => { if (route.name && router.hasRoute(route.name)) { router.removeRoute(route.name) } }) permissionStore.reset() }

这个清理动作是我的一个老朋友项目里出的真实事故。当时测试环境切账号切换得比较频繁,有同事反馈"上个账号的菜单过一会儿又冒出来了",排查到最后就是这个原因:路由表是全局单例,store 清了,路由注册记录没清。

5. 组合实战:一个后台项目里三种方案怎么协同

讲了三个场景,很多人可能还是会问:我到底该用哪种?其实商用后台项目里,它们三个不是互斥的,而是互补的。

5.1 三种方案对比,怎么选

先看一张对比表:

维度接口菜单驱动文件约定式角色权限过滤
路由数据来源后端接口返回views 目录文件前端路由表+权限码
核心优势菜单可后台配置新增页面零成本访问控制安全
核心成本前后端字段约定多额外约定规则需要处理时序与清理
适用项目需要运营配置菜单的中大型后台内部系统、原型迭代快所有需要鉴权的系统
典型配合与权限过滤组合作为组件映射底座与接口菜单组合

在我看来没有绝对的"最佳方案",只有"当前项目阶段最合适的方案"。如果你是给内部团队快速搭一个管理工具,场景二就够用,一个文件就是一个页面,省心省力。如果你的系统面向多个客户,每个客户看到的菜单不同,那必须上场景一,把菜单配置权交到运营手里。不管哪种,只要涉及登录和不同角色,都逃不开场景三的守卫和过滤逻辑。

5.2 混合架构:静态打底 + 接口菜单 + 目录映射组件 + 守卫拦截

我目前维护的中后台项目,最后稳定下来的是这样一套混合架构:

  • 静态路由只放/login/404/dashboard这种不需要权限的基础页面;
  • 用户登录后,后端返回菜单树和权限码列表;
  • 前端用import.meta.glob('/src/views/**/*.vue')建立组件映射,再递归把菜单树转成路由记录;
  • 转出来的路由记录根据权限码再过滤一遍,双重保险;
  • 通过router.addRoute挂载到路由实例上;
  • 守卫里维护一个isDynamicRoutesAdded,用来处理刷新白屏和重复挂载。

组件映射、菜单转换、权限过滤这三个函数单独拆成模块,互不依赖。以后如果后端接口字段调整,只改转换那一层;如果组件目录重构,只改 glob 映射那一层;如果权限模型换了,只改过滤函数。代码是长这样的:

// router/dynamic/index.ts export async function buildDynamicRoutes(menus: MenuData[], permissions: string[]) { const rawRoutes = transformMenuToRoutes(menus) // 场景一:菜单 -> 路由 const filteredRoutes = filterRoutesByPermission( // 场景三:权限过滤 rawRoutes, permissions ) return filteredRoutes }

transformMenuToRoutes 里用的resolveComponent就是场景二的 glob 映射,这样三个场景在同一套代码里各司其职。

5.3 路由、菜单、面包屑、标签页的联动

动态路由挂载是第一步,后面还有一堆联动等着处理。菜单要在路由挂载成功后生成,面包屑要基于to.matched动态生成,标签页要根据路由 name 判断是否缓存。

先说菜单。动态路由是通过router.addRoute加到路由实例里的,这些路由不会自动出现在router.options.routes里,所以你需要自己维护一份菜单数据来源。我的习惯是:transformMenuToRoutes 之前,把后端菜单树单独存一份到 store,菜单组件遍历这份数据渲染;登出时清空。如果直接用router.getRoutes()去遍历菜单,会遇到两个问题:一是路由会带有大量__proto__和内部属性,不适合直接绑到视图;二是顺序不稳定,getRoutes()返回的顺序不是注册顺序,不能当作菜单顺序。

再讲 keep-alive。动态路由最常被忽略的性能问题就是组件缓存失效。比如从用户管理切到角色管理再切回来,每次都会重新请求数据。原因是 keep-alive 的 include 需要匹配路由的 name,如果你的动态路由 name 没有生成规范,或者每次切换账号时 name 变化了,缓存就失效。建议把 name 的生成逻辑固定下来,后端菜单里强制要求 name 字段,或者用路径做哈希。

5.4 一套可以直接复用的动态路由工具函数

每次写新项目都要重写一遍动态路由还挺烦的,我把最核心的工具函数整理成了一个小模块,结构是这样的:

// router/dynamic/helpers.ts /** 1. 建立组件映射表 */ export function createViewModules() { return import.meta.glob('/src/views/**/*.vue') } /** 2. 根据组件字符串找到对应的懒加载函数 */ export function resolveComponent(component: string, viewModules: Record<string, any>) { if (component === 'Layout') return () => import('@/layout/index.vue') return viewModules[`/src/views/${component}.vue`] } /** 3. 菜单树转路由表(递归) */ export function transformMenuToRoutes(menus: MenuData[], viewModules: Record<string, any>): RouteRecordRaw[] { // 逻辑见场景一 } /** 4. 按权限码过滤路由表(递归) */ export function filterRoutesByPermission(routes: RouteRecordRaw[], permissions: string[]): RouteRecordRaw[] { // 逻辑见场景三 } /** 5. 注册动态路由 */ export function registerDynamicRoutes(router: Router, routes: RouteRecordRaw[]) { routes.forEach((route) => { if (route.name && router.hasRoute(route.name)) { router.removeRoute(route.name) } router.addRoute(route) }) } /** 6. 清理动态路由 */ export function clearDynamicRoutes(router: Router, routes: RouteRecordRaw[]) { routes.forEach((route) => { if (route.name && router.hasRoute(route.name)) { router.removeRoute(route.name) } }) }

这套工具函数不依赖任何具体的 UI 库和状态库,拷到任何一个 Vue3 + Vite 项目里都能直接跑。

5.5 实测提醒:这些坑我建议你提前避开

最后把我这几年做动态路由踩过的一些真实问题集中说一下,省得大家再走一遍弯路。

第一个是接口菜单返回的component字段路径拼错。我在开发时遇到过页面一直白屏,控制台也不报错,后来打印了resolveComponent的入参才发现,后端返回的是system/user/index,而我的目录下文件名是SysUser.vue,中间隔了一层重命名。这个问题的最好解法是从项目第一天就强制统一目录命名规范,后端返回的 component 标识必须严格对应src/views下的文件相对路径。

第二个是嵌套路由父级没有组件导致子页面渲染不出来。地址显示正常,路由也匹配到了,但页面区域空白。排查到最后是父路由没有配置component: Layout,vue-router 会正常匹配但无法渲染嵌套视图。实现动态路由时,只要遇到数据有 children 的节点,就要确认它的 component 解析到了能渲染子路由的布局组件。

第三个是环境变量导致的构建差异。动态路由会用import.meta.glob,它是 Vite 的编译期能力,在构建测试环境和生产环境时没问题,但如果有人把 prod 环境配成vite build --mode test,某些环境文件的加载路径可能会影响接口菜单的域名,导致登录后动态路由拉取失败,页面卡在守卫的 loading 状态里。这个问题和动态路由本身无关,但排查起来很容易绕晕,建议把路由初始化失败的兜底提示做得明确一点。

第四个是动态页面做了缓存后,权限变化不生效。比如用户 A 有某个菜单权限,访问过后页面被 keep-alive 缓存;管理员把 A 的权限收回,A 再次访问旧路径时,如果动态路由过滤已经把他拦在 404 之外还好,怕的是路由还在、页面缓存还在,A 直接看到了旧数据。处理方式是在登出时不仅清 store,还要调用keep-alive对应的缓存清理逻辑,或者在权限变化后强制刷新整个页面。

动态路由这套东西,每个项目细节都不太一样,但核心思路就这些。我自己的经验是:第一版先别急着上最重的那套方案,用静态路由 + 权限守卫把项目跑通,等后端菜单接口稳定了,再把接口驱动那层替换进去。这样前期开发不被接口阻塞,后期切换成本也不高。至于场景二的文件约定式,单独使用的话清爽,组合使用的话作为组件映射底座,是目前我在 Vite 项目里最依赖的姿势。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询