SPA应用在登录状态无刷新状态实时执行权更新
最近有空,撸了一下曾经工作中一直想优化的功能:SPA应用在登录状态无刷新状态实时执行权更新。
因为之前项目中的用户前端权限有三种(页面菜单、按钮增删改查、接口数据权限),这次先优化第一种页面菜单。
demo的技术栈是:ts5.4、vue3、vue-router4、vite5.4、pinia2,项目目录如下:
其中全量路由列表常量保存在@/constants/router/Routers.ts里:
export const Routers = [ { path: '/parentA', name: 'parentA', component: () => import('@/views/ParentA/ParentAView.vue'), children: [ { path: 'childA', name: 'parentAChildA', component: () => import('@/views/ParentA/ParentAChildAView.vue'), meta: {} }, { path: 'childB', name: 'parentAChildB', component: () => import('@/views/ParentA/ParentAChildBView.vue'), meta: {} } ] }, { path: '/parentB', name: 'parentB', component: () => import('@/views/ParentB/ParentBView.vue'), children: [ { path: 'childA', name: 'parentBChildA', component: () => import('@/views/ParentB/ParentBChildAView.vue'), meta: {} }, { path: 'childB', name: 'parentBChildB', component: () => import('@/views/ParentB/ParentBChildBView.vue'), meta: {} } ] } ]
更新路由hook核心代码:src/hooks/routers.ts
import type { Router, RouteRecordRaw } from 'vue-router' import { Routers as TotalTemplateRouters } from '@/constants/router/Routers' let menus: any[] = [] function removeRoutePro(routerInstance: Router, currentRoutes: RouteRecordRaw[], name: string) { routerInstance.removeRoute(name); const index = currentRoutes.findIndex((route) => route.name === name); index >= 0 && currentRoutes.splice(index, 1); } function mergeRouterTree( routerInstance: Router, samelevelTemplateRoutes: RouteRecordRaw[], currentRoutes: RouteRecordRaw[], currentRouteParentRoute?: RouteRecordRaw ) { ;(samelevelTemplateRoutes as RouteRecordRaw[])?.forEach?.((TemplateRoute) => { const TemplateRouteChildren = TemplateRoute.children || []; const currentRoute = currentRoutes.find((currentRoute) => currentRoute.name === TemplateRoute.name) const currentRouteMenu = menus.find((menu) => menu.name === TemplateRoute.name) /** * *********************** 一、如果没有菜单权限,执行删除**************************** */ if (!currentRouteMenu) { TemplateRoute.name && removeRoutePro(routerInstance, currentRoutes, TemplateRoute.name as string); return } /** * *********************** 二、如果有菜单权限 ************************************* */ // 1) 当前路由树中不存在此路由,执行新增 if (!currentRoute) { // 如果有children就继续递归 const children = [...TemplateRouteChildren]; const templateRouteCopy = {...TemplateRoute, children}; if (currentRouteParentRoute?.name) { routerInstance.addRoute(currentRouteParentRoute.name, templateRouteCopy) } else { routerInstance.addRoute(templateRouteCopy) } mergeRouterTree(routerInstance, TemplateRouteChildren, children, templateRouteCopy); return } // 2) 当前路由树中存在此路由,执行更新children // 如果有children就继续递归 TemplateRouteChildren?.length > 0 && mergeRouterTree(routerInstance, TemplateRouteChildren, currentRoute.children || [], currentRoute); }) } // 需要测试parentA=>childrenB更新到parentA=>childrenC的情况下childrenB是否被消除。 function updateRouterTree(routerInstance: Router) { mergeRouterTree( routerInstance, TotalTemplateRouters, routerInstance.getRoutes() ) /** * ********如果当前访问页面的route在更新routeTree之后被删除了,需要replace到无权限/404页面。************ */ // code************* } export const useRouters = async (routerInstance: Router, _menus: any[] = []) => { menus = _menus updateRouterTree(routerInstance) return { } }
后续通过在合适的时间点去调用useRouters(routerInstance, menus);方法更新Router树,其中请求后台获取的menus数据的结构通常是扁平的:
// 用例:
useRouters(routerInstance, [ { path: '/parentA', name: 'parentA', }, { path: 'childB', name: 'parentAChildB', meta: {} }, { path: '/parentB', name: 'parentB', }, { path: 'childB', name: 'parentBChildB', meta: {} } ]); // Router树更新为: [ '/', '/parentA', '/parentA/childB', '/parentB', '/parentB/childB'] useRouters(routerInstance, [ { path: '/parentA', name: 'parentA', }, { path: 'childA', name: 'parentAChildA', meta: {} }, { path: '/parentB', name: 'parentB', }, { path: 'childA', name: 'parentBChildA', meta: {} } ]); // Router树更新为: [ '/', '/parentA', '/parentA/childA', '/parentB', '/parentB/childA'] // 通过merge的形式去掉了第一次新增的'parentAChildB'和'parentBChildB'子路由
用例中使用mock数据调用了两次useRouters方法,
第一次Router树更新为: [ '/', '/parentA', '/parentA/childB', '/parentB', '/parentB/childB']
第二次Router树更新为: [ '/', '/parentA', '/parentA/childA', '/parentB', '/parentB/childA']
useRouters通过merge的形式去掉了第一次新增的'parentAChildB'和'parentBChildB'子路由,访问这两个路由会重定向到404页面,
如果需要区分“访问路由资源404”和“缺失访问路由权限”可以进一步完善在updateRouterTree方法逻辑,并配合在404页面中补全逻辑。