接下看下如何实现动态加载路由与菜单
开始之前我们先安装全局状态管理pinia
npmipinia-s然后main.ts中引入,同时将element-plus的Icon全局注册(这里后续就能直接使用图标了)
import{createApp}from"vue";importAppfrom"./App.vue";importrouterfrom"./router";importElementPlusfrom"element-plus";import"element-plus/dist/index.css";import{createPinia}from"pinia";import"./index.css";import*asElementPlusIconsVuefrom"@element-plus/icons-vue";constapp=createApp(App);//将element-plus的图标注册到appfor(const[key,component]ofObject.entries(ElementPlusIconsVue)){app.component(key,component);}constpinia=createPinia();app.use(ElementPlus);app.use(router);app.use(pinia);//等待路由初始化完成后再挂载,确保守卫beforeach可以使用piniaawaitrouter.isReady();app.mount("#app");布局看一下页面布局,分为顶部导航栏(navbar)+左侧菜单栏(sidebar)+主要内容(appmain)
项目新建layout文件夹来存放布局组件
--layout--components--AppMain--index.vue--NavBar--index.vue--SideBar--index.vue--index.js--index.vue在index.vue引入它们
在api/menu/index.ts中配置调用获取路由及权限的接口
exporttypeMenuDto={};这里什么都不用传
新建store/index.ts目录用来存放全局数据,菜单列表数据,权限列表数据,以及是否折叠菜单等等,同时调用getInfo接口获取数据
import{defineStore}from"pinia";import{getInfo}from"../api/menu/index";import{AppStoreState}from"./types";exportdefaultdefineStore("appStore",{state:():AppStoreState=>{return{menuList:[],isCollapse:false,permissions:[],};},actions:{asyncgetInfo(){const{data}=awaitgetInfo({});this.menuList=data.routers;this.permissions=data.permissions;},},});其中AppStoreState
exporttypeMenuList={id:numberparent_id:numbertitle:stringpath:stringcomponent:stringicon:stringorder_num:numberstatus:booleanmenu_type:1|2|3children:MenuList[]meta:{title:stringcatch:numberhidden:boolean}}exporttypeAppStoreState={menuList:MenuList[]isCollapse:booleanpermissions:string[]}动态获取路由我们可以通过router.addRoute方式动态添加子路由,这里我们需要根据后端返回的组件component字段来创建目录,比如system/role/index就在views目录下新建system/role/index.vue
添加之前需要将后端返回的菜单列表数据转换为符合Vue路由的格式,因为后端返回的组件路径是个字符串,VueRouter的component是不能直接使用的,这里我们在utils文件夹新建filterRouters.ts,
//匹配views里面所有的.vue文件constmodules=import.meta.glob("../views/**/*.vue");//将本地的路由与后端返回的路由进行匹配exportconstloadView=(view:any)=>{letres;for(constpathinmodules){constdir=path.split("views/")[1].split(".vue")[0];if(dir===view){res=()=>modules[path]();}}returnres;};exportconstfilterRoute=(data:any)=>{data.forEach((item:any)=>{if(item.children.length>0){deleteitem.component;filterRoute(item.children);}else{item.component=loadView(item.component);//item.redirect="/404";}});returndata;};然后再新建hooks/useHandleRouter.ts,进行动态添加路由的逻辑
import{RouteRecordRaw,Router}from"vue-router";importuseAppStorefrom"@/store/index"import{filterRoute}from"@/utils/filterRouters";exportconstuseHandleRouter=(router:Router)=>{//设置白名单,直接放行constwriteLists=["Login"];router.beforeEach(async(to,_from,next)=>{if(writeLists.includes(to.nameasstring)){next();return;}constappStore=useAppStore();//已经获取菜单路由直接放行if(appStore.menuList.length){next()return;}//获取菜单路由列表try{awaitappStore.getInfo();//处理成符合vue路由格式的路由constrouters=filterRoute(appStore.menuList);//循环添加路由到父路由Index下routers.forEach((route:RouteRecordRaw)=>{router.addRoute("Index",route);});////添加完路由需要重新执行一次路由跳转,否则会出现空白页面next({...to,replace:true});}catch(error){//如果接口出错比如token过期继续往下走next()}});}最后在router/index引入使用,同时添加一个/404的路由,当用户访问的路由不存在时,会跳转到/404路由
import{createRouter,createWebHashHistory,RouteRecordRaw}from"vue-router";import{useHandleRouter}from"@/hooks/useHandleRouter";exportconstroutes:RouteRecordRaw[]=[{path:"/",name:"Index",redirect:"/index",component:()=>import(/*webpackChunkName:"index"*/"@/layout/index.vue"),children:[{path:"/index",component:()=>import("@/views/index.vue"),name:"Home",meta:{title:"首页"},},],},{path:"/login",name:"Login",component:()=>import(/*webpackChunkName:"login"*/"@/views/login/index.vue"),},{path:"/:pathMatch(.*)*",component:()=>import("@/views/error/404.vue"),},];constrouter=createRouter({history:createWebHashHistory(),scrollBehavior(_to,_from,savedPosition){if(savedPosition){returnsavedPosition;}else{return{top:0};}},routes,});useHandleRouter(router);exportdefaultrouter;到这里我们便完成了路由的动态加载
接下来我们来完成菜单侧边栏(SideBar)部分,这里我们使用element-plus中的菜单组件el-menu,然后通过判断后端返回的路由类型是菜单还是目录来分别使用el-menu-item和el-menu-sub。
新建layout/SideBar/index.vue来编写侧边栏的代码
exportconstdealRoutePath=(path:string)=>{if(!path)return"";constpathArr=path.split("/");returnpathArr.slice(-1)[0];};除此之外,SideBarItem组件中引入了自身SideBarItem实现了组件的递归,这样就可以保证无论菜单层级多少都可以对应展示