权限路由动态注册使用说明和完整示例

发布时间:2026/7/22 14:22:43
权限路由动态注册使用说明和完整示例 一、 核心原理与使用说明路由分层将路由分为constantRoutes无需权限的静态路由如登录、404和asyncRoutes需要权限的动态路由。组件懒加载Vite 不支持require.context必须使用import.meta.glob批量导入views下的组件。动态挂载使用 Vue Router 4 提供的router.addRoute()将过滤后的路由添加到指定的父级布局Layout下。刷新防丢失页面刷新会导致内存中的动态路由丢失。必须在router.beforeEach中判断如果已登录但路由未生成则重新拉取权限并生成路由最后使用next({ ...to, replace: true })重新触发导航。菜单联动侧边栏菜单应直接读取生成后的路由表结合meta字段保证路由与菜单的绝对一致。二、 完整实战示例1. 定义路由表 (src/router/routes.ts)importtype{RouteRecordRaw}fromvue-router// 1. 静态路由无需权限exportconstconstantRoutes:RouteRecordRaw[][{path:/login,name:Login,component:()import(/views/login/index.vue),meta:{hidden:true}},{path:/404,name:NotFound,component:()import(/views/error/404.vue),meta:{hidden:true}}]// 2. 动态路由需要权限// 注意必须配置 name否则重置路由时可能会出问题exportconstasyncRoutes:RouteRecordRaw[][{path:/system,name:System,component:()import(/layouts/DefaultLayout.vue),redirect:/system/user,meta:{title:系统管理,icon:setting,roles:[admin]},children:[{path:user,name:SystemUser,component:()import(/views/system/user/index.vue),meta:{title:用户管理,roles:[admin]}},{path:role,name:SystemRole,component:()import(/views/system/role/index.vue),meta:{title:角色管理,roles:[admin]}}]},// ️ 404 兜底路由必须放在最后动态添加{path:/:pathMatch(.*)*,redirect:/404,meta:{hidden:true}}]2. 封装权限 Store (src/stores/modules/permission.ts)import{ref}fromvueimport{defineStore}frompiniaimporttype{RouteRecordRaw}fromvue-routerimport{asyncRoutes,constantRoutes}from/router/routes// 使用 Vite 的 import.meta.glob 预加载所有视图组件constmodulesimport.meta.glob(/views/**/*.vue)exportconstusePermissionStoredefineStore(permission,(){constroutesrefRouteRecordRaw[]([])constisRoutesGeneratedref(false)// 核心根据角色过滤路由functionfilterAsyncRoutes(routeList:RouteRecordRaw[],roles:string[]):RouteRecordRaw[]{constres:RouteRecordRaw[][]routeList.forEach(route{consttmp{...route}if(hasPermission(tmp,roles)){if(tmp.children){tmp.childrenfilterAsyncRoutes(tmp.children,roles)}// 将字符串组件路径转换为真实的懒加载组件if(typeoftmp.componentstring){tmp.componentmodules[/src/views/${tmp.component}.vue]}res.push(tmp)}})returnres}functionhasPermission(route:RouteRecordRaw,roles:string[]):boolean{if(route.meta?.roles){returnroles.some(role(route.meta!.rolesasstring[]).includes(role))}returntrue}// 生成动态路由并缓存functiongenerateRoutes(roles:string[]):RouteRecordRaw[]{letaccessedRoutes:RouteRecordRaw[]if(roles.includes(admin)){accessedRoutesasyncRoutes||[]}else{accessedRoutesfilterAsyncRoutes(asyncRoutes,roles)}routes.valueconstantRoutes.concat(accessedRoutes)isRoutesGenerated.valuetruereturnaccessedRoutes}// 退出登录时重置functionresetRoutes(){routes.value[]isRoutesGenerated.valuefalse}return{routes,isRoutesGenerated,generateRoutes,resetRoutes}})3. 配置路由守卫 (src/router/index.ts)import{createRouter,createWebHistory}fromvue-routerimport{constantRoutes}from./routesimport{useUserStore}from/stores/modules/userimport{usePermissionStore}from/stores/modules/permissionconstroutercreateRouter({history:createWebHistory(import.meta.env.BASE_URL),routes:constantRoutes,scrollBehavior:()({top:0})})constwhiteList[/login]router.beforeEach(async(to,from,next){constuserStoreuseUserStore()constpermissionStoreusePermissionStore()if(userStore.token){if(to.path/login){next({path:/})}else{// 核心逻辑已登录但尚未生成动态路由if(!permissionStore.isRoutesGenerated){try{// 1. 获取用户信息包含角色/权限awaituserStore.fetchUserInfo()// 2. 生成可访问的路由表constaccessRoutespermissionStore.generateRoutes(userStore.roles)// 3. 动态挂载到路由实例accessRoutes.forEach(route{router.addRoute(route)})// 4. ️ 必须重新导航否则新添加的路由不会立即生效next({...to,replace:true})}catch(error){// 获取用户信息失败如 Token 过期重置状态并跳转登录awaituserStore.logout()next(/login?redirect${to.path})}}else{next()}}}else{// 未登录if(whiteList.indexOf(to.path)!-1){next()}else{next(/login?redirect${to.path})}}})exportdefaultrouter三、 进阶后端返回路由表模式如果后端直接返回路由 JSON 结构如component: system/user/index只需修改generateRoutes中的组件解析逻辑// 后端返回的数据结构示例// { path: /system, component: DEFAULT_LAYOUT, children: [{ path: user, component: system/user/index }] }functionmapBackendRoutes(serverRoutes:any[]):RouteRecordRaw[]{returnserverRoutes.map(item{constroute:any{...item}// 处理布局组件if(item.componentDEFAULT_LAYOUT){route.component()import(/layouts/DefaultLayout.vue)}elseif(item.component){// 动态匹配 views 下的组件constcomponentPath/src/views/${item.component}.vueroute.componentmodules[componentPath]}// 递归处理子路由if(item.children){route.childrenmapBackendRoutes(item.children)}returnroute})}四、 避坑指南刷新白屏/404确保在beforeEach中调用了next({ ...to, replace: true })因为addRoute不会触发当前页面的重新渲染。404 路由位置/:pathMatch(.*)*必须在动态路由全部添加完毕后才生效。如果一开始就写在静态路由里会导致动态路由还没加载就被 404 拦截。Vite 路径匹配import.meta.glob返回的 key 是绝对路径如/src/views/user/index.vue拼接时注意不要漏掉/src/前缀。路由 Name 唯一性动态路由的name必须全局唯一否则addRoute会覆盖同名路由导致页面错乱。