在开发权限系统时,根据后端返回的菜单列表动态添加路由是一个非常普遍的需求。这可以实现根据用户权限动态加载可访问的页面。在上一篇文章中,我们已经了解了如何渲染侧边栏菜单。本篇文章我们将重点介绍如何优化动态路由的添加过程。
将菜单列表转换为路由格式
学过 Vue 的人都知道,Vue 路由包含 name、path、component 等属性,其中 component 属性是一个函数返回一个模块,例如:
1 2 3 4 5
| { path: '/', name: 'Layout', component: () => import( '../layout/index.vue'), }
|
但是后端返回的却是一个字符串,例如 aa/bb,因此我们需要一个函数将后端返回的菜单列表处理成路由格式。在 utils 目录下新建 filterRoute.ts 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const modules = import.meta.glob('../views/**/*.vue');
export const loadView = (view: any) => { let res; for (const path in modules) { const dir = path.split('views/')[1].split('.vue')[0]; if (dir === view) { res = () => modules[path](); } } return res; };
export const filterRoute = (data: any) => { data.forEach((item: any) => { if (item.children?.length > 0) { delete item.component; filterRoute(item.children); } else { item.component = loadView(item.component); } }); return data; };
|
其中,import.meta.glob(‘../views/**/*.vue’) 可以匹配到 views 目录下所有后缀为 .vue 的文件。然后通过对比后端返回的 component 与 views/ 后面的路径来生成 Vue 路由中 component 所需要的格式。
同时,filterRoute 函数中如果菜单是父菜单,则其不能有 component 属性。
根据以上规则可以看出,如果菜单 component 配置了 aa/bb 则我们需要在 views 目录下创建 aa/bb.vue 文件才能匹配当前组件路径。
引入 Pinia
然后在 main.ts 这个文件里注册:
1 2 3 4 5
| import { createPinia } from 'pinia'; const app = createApp(App);
const pinia = createPinia(); app.use(pinia).use(router).mount('#app');
|
新建 store/index 用于存放 Pinia 管理的状态,同时这里我们定义了获取菜单的方法 GenerateRoutes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { defineStore } from 'pinia'; import { getMenuList } from '@/http/menu/index'; import router from '@/router'; import { MenuVo } from '@/http/menu/types/menu.vo';
type StoreState = { isCollapse: boolean; menuList: MenuVo[]; };
export default defineStore('home', { state: (): StoreState => { return { isCollapse: false, menuList: [], }; }, actions: { async GenerateRoutes() { const { data } = await getMenuList({}); this.menuList = data; return data; }, }, });
|
然后在路由守卫 beforeEach 中使用 addRoute 进行动态路由添加,同时定义一个白名单路由列表,这里暂时只有一个 login 页面,表示直接放行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import home from '@/store'; import { nextTick } from 'vue'; import { filterRoute } from '@/utils/filterRoute';
...
const whiteLists = ['login'];
router.beforeEach(async (to, from, next) => { if (whiteLists.includes(to.name as string)) { next(); return; }
await nextTick(); const homeStore = home(); if (homeStore.menuList.length) { next(); return; }
const data = await homeStore.GenerateRoutes(); const routers = filterRoute(data); routers.forEach((route: RouteRecordRaw) => { router.addRoute('Layout', route); });
next({ ...to, replace: true }); });
|
PS:
这里在获取 pinia 中数据时,先执行了 await nextTick(); 是因为此时如果直接拿 pinia 中数据是拿不到的,因为 pinia 还没有初始化完毕 next({ …to, replace: true }) 再次加载当前路由,即重新执行 beforeEach ,
因为第一次进入 beforeEach 函数中的时候还没有加载动态路由,所以我们需要在添加完路由后重新加载当前路由以触发 beforeEach 钩子函数.
路由跳转
我们模拟修改了菜单表中子菜单 2.1 的 component 为 test2/index。
相应地,我们需要在 views 目录下新建 test2/index.vue 文件
1 2 3 4 5
| <template> <div>test2</div> </template>
<script lang="ts" setup></script>
|
接下来,我们需要在 layout/components/sidebar.vue 文件中处理菜单选择事件。
其中,getPath 函数中的 d 参数代表当前菜单的父菜单和子菜单的索引数组,即父菜单的 path 和子菜单的 path,例如 [‘aa’, ‘bb’],我们可以根据它们进行相应的路由跳转。
最后,当我们点击子菜单 2.1 时,就会发现这个页面被渲染出来了。
写在后面
到这里动态路由添加基本完成了。当然,可能还会有一些细枝末节需要处理。