Vue3动态菜单和路由


后台系统里面,菜单和路由一般都不是写死的,而是根据后端返回的权限动态生成,这里记录一下 Vue3 中动态菜单和动态路由的处理方式

后台系统和普通官网不太一样,普通官网路由基本可以写死,但是后台系统通常需要根据账号权限显示不同菜单。

比如管理员可以看到所有菜单,普通账号只能看到部分菜单。

所以这里就会涉及两个问题:

  1. 左侧菜单怎么显示
  2. 页面路由怎么注册

后端菜单

一般后端返回的菜单都是树形结构,类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
menuId: '1',
parentId: '0',
menuTitle: '演示页面',
menuPath: '/demo',
menuDisplayStatus: 1,
menuType: 1,
children: [
{
menuId: '2',
parentId: '1',
menuTitle: '表格示例',
menuPath: '/demo/table',
menuDisplayStatus: 1,
menuType: 1
}
]
}
]

前端要做的事情就是把这份菜单转换成侧边栏和路由。

收集页面

因为使用的是 Vite,所以动态收集页面使用的是 import.meta.glob

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

这行代码会把 src/views/pages 下面的 .vue 页面全部收集起来。

比如页面文件是:

1
src/views/pages/demo/table/index.vue

那么后面就可以通过路径找到这个页面。

路径处理

后端返回的 menuPath 不一定刚好等于页面文件路径,所以这里需要做一层处理。

我这里约定普通页面路径为:

1
/src/views/pages + menuPath + /index.vue

比如:

1
menuPath: /demo/table

对应页面就是:

1
/src/views/pages/demo/table/index.vue

如果菜单路径和真实文件路径不一致,也可以增加一个 componentPath 字段专门指定页面路径。

这样菜单显示路径和页面文件路径就可以分开。

生成路由

菜单本身是树形结构,但是 Vue Router 最终需要的是路由配置。

所以需要递归菜单树,把真正的叶子页面转成路由:

1
2
3
4
5
6
7
8
9
{
path: menuItem.menuPath,
name: String(menuItem.menuId || menuItem.menuPath),
meta: {
title: menuItem.menuTitle,
menuId: menuItem.menuId
},
component: createRouteComponent(menuItem)
}

这里有几个点需要注意。

path

path 使用后端返回的 menuPath

因为这个路径是用户真正访问的地址,也是侧边栏点击后跳转的地址。

name

name 不建议随便写。

我这里优先使用 menuId,因为动态路由后面可能需要删除、重置、判断是否已经存在。

如果 name 不稳定,后面处理会比较麻烦。

meta

meta 里面主要放页面显示需要用到的信息。

比如:

1
2
3
4
5
6
meta: {
title: menuItem.menuTitle,
titleKey: menuItem.menuI18nKey,
id: currentRootMenuId,
menuId: menuItem.menuId
}

标题、菜单 id、顶级菜单 id 都可以放进去,后面标签页、面包屑、菜单高亮都能用到。

component

动态路由里面最重要的是组件匹配。

大概逻辑是:

1
2
3
4
5
6
7
8
9
10
const pagePathCandidates = [
`/src/views/pages${menuPath}/index.vue`,
`/src/views/pages${menuPath}.vue`
];

for (const pagePath of pagePathCandidates) {
if (modules[pagePath]) {
return modules[pagePath];
}
}

如果匹配不到页面,就跳到 404 页面,避免整个路由注册失败。

侧边栏菜单

路由需要完整菜单,但是侧边栏不一定要显示全部。

比如按钮权限、隐藏菜单都不应该显示在侧边栏里面。

所以还需要过滤一次:

1
2
3
4
5
6
7
8
const filterHidden = (menuList = []) => {
return menuList
.filter((item) => Number(item.menuDisplayStatus) === 1 && Number(item.menuType) === 1)
.map((item) => ({
...item,
children: filterHidden(item.children || [])
}));
};

这样侧边栏只显示需要展示的菜单。

Pinia保存

菜单和权限需要保存到 Pinia 中:

1
2
3
4
5
6
7
8
state: () => {
return {
menuList: [],
showMenu: [],
apiList: [],
activeIndex: '0'
};
}

其中:

  1. menuList 保存完整菜单
  2. showMenu 保存侧边栏菜单
  3. apiList 保存按钮权限
  4. activeIndex 保存当前选中菜单

并且需要开启持久化,因为刷新页面的时候还要恢复动态路由。

总结

动态菜单和动态路由的核心其实就三步:

  1. 后端返回菜单树
  2. 前端过滤出侧边栏菜单
  3. 前端把菜单转换成 Vue Router 路由

这里面比较容易踩坑的是页面路径匹配和刷新后路由丢失。

路径匹配可以通过 componentPath 兜底,刷新恢复下一篇再单独记录。

以上就是我对 Vue3 动态菜单和路由的一些理解,如有错误,欢迎大佬指出。

-------------本文结束感谢您的阅读-------------