【若依】15、动态菜单加载思路
整体思路
- 当用户登录成功之后,前端会自动发送一个请求,去后端查询当前这个登录成功的用户的动态菜单。具体的思路就是根据当前登录成功的用户 id,去 sys_user_role 表中查询到这个用户的角色 id,然后在根据角色 id 去 sys_role_menu 表中查询到菜单 id,然后再根据菜单 id 去 sys_menu 表中查询到具体的菜单数据。
- 前端定义了一个前置路由导航守卫,当发生页面跳转的时候,这个路由导航守卫会监听到所有的页面跳转,监听到之后,会去检查是否需要服务端返回的动态菜单数据,如果需要的话,就去服务端加载,加载到之后,渲染侧边栏菜单,同时将菜单项都加入到 router 中。
菜单表细节
is_frame
- 1 表示不是外链:这个菜单将来点击之后,会在当前系统的一个新的选项卡中打开。
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
{ "name": "Http://www.yueyazhui.top", "path": "/", "hidden": false, "component": "Layout", "meta": { "title": "TienChin健身官网", "icon": "guide", "noCache": false, "link": null }, "children": [ { "name": "Www.yueyazhui.top", "path": "www.yueyazhui.top", "hidden": false, "component": "InnerLink", "meta": { "title": "TienChin健身官网", "icon": "guide", "noCache": false, "link": "http://www.yueyazhui.top" } } ] }
- 0 表示是外链:这个菜单将来点击之后,会在一个浏览器的新选项卡中打开。
1 2 3 4 5 6 7 8 9 10 11 12
{ "name": "Http://www.yueyazhui.top", "path": "http://www.yueyazhui.top", "hidden": false, "component": "Layout", "meta": { "title": "TienChin健身官网", "icon": "guide", "noCache": false, "link": "http://www.yueyazhui.top" } }
区别:
- 前者有 children,后者无 children(但是最终渲染出来的侧边栏菜单是一样的)。
- 前者是需要在当前系统中展示这个外链的内容,所以需要 children,children 会给出来外链展示所需要的 component 组件(InnerLink),将来这个外链,就在 InnerLink 这个组件中展示出来。
- 后者不需要 children,因为后者的菜单项相当于就是一个超链接,点击之后,直接就在浏览器中打开一个新的选项卡去展示。
menu_type
正常来说: M:这个菜单项是一个目录。 C:这个菜单项是一个具体的菜单。 但是有一些特殊情况: 如果一个菜单项,是 C 类型的,那么它必须有一个 parent,如果没有,系统会自动给添加上 Layout 作为其 parent。例如下面这个,角色管理,作为一级菜单,它本身是没有 parent(parent_id 为 0),此时服务端会自动为其添加一个 parent,并将 Layout 作为 parent 的 component。将来前端在渲染的时候,如果发现这个菜单项只有一个 children,那么就会自动渲染其 children,而不会渲染 parent 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
{ "path": "/", "hidden": false, "component": "Layout", "children": [ { "name": "Role", "path": "role", "hidden": false, "component": "system/role/index", "meta": { "title": "角色管理", "icon": "peoples", "noCache": false, "link": null } } ] }
如果一个菜单项,是 M 类型的,那么它就没有 parent,此时展示的时候,如果不是外链,可能会有一些问题。
1 2 3 4 5 6 7 8 9 10 11 12
{ "name": "Role", "path": "/role", "hidden": false, "component": "system/role/index", "meta": { "title": "角色管理", "icon": "peoples", "noCache": false, "link": null } }
这个渲染的时候,直接就是渲染它自己。将来展示的组件缺乏一个 parent,所以,如果点击这个菜单项,这个菜单项的内容会直接填满整个页面,因为相当于当前页面跟登录页面以及项目的主页面平级了。
正常的菜单数据
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
{ "name": "Tool", "path": "/tool", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统工具", "icon": "tool", "noCache": false, "link": null }, "children": [ { "name": "Build", "path": "build", "hidden": false, "component": "tool/build/index", "meta": { "title": "表单构建", "icon": "build", "noCache": false, "link": null } }, { "name": "Gen", "path": "gen", "hidden": false, "component": "tool/gen/index", "meta": { "title": "代码生成", "icon": "code", "noCache": false, "link": null } }, { "name": "Swagger", "path": "swagger", "hidden": false, "component": "tool/swagger/index", "meta": { "title": "系统接口", "icon": "swagger", "noCache": false, "link": null } } ] }
服务端动态菜单 JSON
服务端返回动态菜单 JSON 的接口,整体上来说,分为两个 步骤:
- 去数据库中查询菜单数据。
- 将查到的菜单数据,构造为前端需要的 JSON 格式。
1 2 3 4 5 6 7 8 9 10 11
/** * 获取路由信息 * * @return 路由信息 */ @GetMapping("getRouters") public AjaxResult getRouters() { Long userId = SecurityUtils.getUserId(); List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId); return AjaxResult.success(menuService.buildMenus(menus)); }
第一步,去数据库中查询菜单 JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/** * 根据用户ID查询菜单 * * @param userId 用户名称 * @return 菜单列表 */ @Override public List<SysMenu> selectMenuTreeByUserId(Long userId) { List<SysMenu> menus = null; if (SecurityUtils.isAdmin(userId)) { menus = menuMapper.selectMenuTreeAll(); } else { menus = menuMapper.selectMenuTreeByUserId(userId); } return getChildPerms(menus, 0); }
如果当前用户是 admin,说明是超级管理员,那么此时直接查询所有的菜单数据即可。 如果不是 admin,那么就根据当前登录用户的 id 去查询菜单数据即可。 menus 是查询到的没有进行菜单层级处理的一个集合。 getChildPerms 方法则是通过一个递归操作,将菜单的层级管理给建立起来。 第二步,构建菜单
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
/** * 构建前端路由所需要的菜单 * * @param menus 菜单列表 * @return 路由列表 */ @Override public List<RouterVo> buildMenus(List<SysMenu> menus) { List<RouterVo> routers = new LinkedList<RouterVo>(); for (SysMenu menu : menus) { RouterVo router = new RouterVo(); router.setHidden("1".equals(menu.getVisible())); router.setName(getRouteName(menu)); router.setPath(getRouterPath(menu)); router.setComponent(getComponent(menu)); router.setQuery(menu.getQuery()); router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); List<SysMenu> cMenus = menu.getChildren(); if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())) { router.setAlwaysShow(true); router.setRedirect("noRedirect"); router.setChildren(buildMenus(cMenus)); } else if (isMenuFrame(menu)) { router.setMeta(null); List<RouterVo> childrenList = new ArrayList<RouterVo>(); RouterVo children = new RouterVo(); children.setPath(menu.getPath()); children.setComponent(menu.getComponent()); children.setName(StringUtils.capitalize(menu.getPath())); children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); children.setQuery(menu.getQuery()); childrenList.add(children); router.setChildren(childrenList); } else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) { router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); router.setPath("/"); List<RouterVo> childrenList = new ArrayList<RouterVo>(); RouterVo children = new RouterVo(); String routerPath = innerLinkReplaceEach(menu.getPath()); children.setPath(routerPath); children.setComponent(UserConstants.INNER_LINK); children.setName(StringUtils.capitalize(routerPath)); children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); childrenList.add(children); router.setChildren(childrenList); } routers.add(router); } return routers; }
从这段代码中,可以看出来,返回的动态菜单 JSON 一共分为了四种情况。 常规数据:
- 可见性
- name:
- 正常来说,name 都是 path 首字母大写;
- 特殊情况就是如果当前类型为 C 并且是一级菜单而且还不是外链,这个时候会自动为这个菜单项生成一个 parent,这个 parent 的 name 为空字符串;例如:menu_type 的第一种情况。
- path:
- 不是一级菜单,并且还是一个内链打开外网(在当前系统中,新开一个选项卡,打开外部链接):去除掉 path 中的 http 或者 https 即可。
- 非外链的一级目录并且是 M 类型,此时 path 就在数据库查出来的 path 前加上 /。
- 非外链的一级目录并且是 C 类型,此时 path 就是 /;例如:menu_type 的第一种情况。
- 其他情况就直接返回菜单项即可。
- 对于正常的菜单数据而言,parent 实际上就是走第二个 if,children 实际上是不会进入到任何分支中,直接返回的。
- component:
- Layout:项目的主页面。
- Inner_Link:展示外链的组件。
- Parent_View:非一级目录的其他目录对应的 component 就是 Parent_View。
- 首先定义了一个默认的 component,就是 Layout。
- 如果当前菜单项有 component,并且当前菜单项还不是一个内部跳转的菜单,那么这个 component 就是从数据库中实际查询到的 component。
- 如果自己没有 component,并且还不是一级菜单,并且还是一个内链(想在当前系统中打开外部链接),设置默认的组件为 Inner_Link。
- 如果自己没有 component,并且还有子菜单,那么就设置当前菜单的 component 为 Parent_View。
- query、meta 都按实际情况来。
- 如果当前菜单有 children,那么递归处理 children。
- 如果当前菜单是 C 类型的,并且还不是外链还是一级菜单,那么就自动给这个菜单项加一个 children(menu_type 中的第一种情况)。
- 如果是一级菜单并且想在内部选项卡中展示一个外部页面,对应 is_frame 中的第一种情况,也就是会自动生成一个 children。
- 三个分支都没进来,说明就没有 children,is_frame 中的第二种情况,menu_type 中的第二种情况。
动态菜单思路梳理
整体上,菜单分为了四种情况:
有父有子
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
{
"name": "System",
"path": "/system",
"hidden": false,
"redirect": "noRedirect",
"component": "Layout",
"alwaysShow": true,
"meta": {
"title": "系统管理",
"icon": "system",
"noCache": false,
"link": null
},
"children": [
{
"name": "User",
"path": "user",
"hidden": false,
"component": "system/user/index",
"meta": {
"title": "用户管理",
"icon": "user",
"noCache": false,
"link": null
}
},
{
"name": "Menu",
"path": "menu",
"hidden": false,
"component": "system/menu/index",
"meta": {
"title": "菜单管理",
"icon": "tree-table",
"noCache": false,
"link": null
}
},
{
"name": "Dept",
"path": "dept",
"hidden": false,
"component": "system/dept/index",
"meta": {
"title": "部门管理",
"icon": "tree",
"noCache": false,
"link": null
}
},
{
"name": "Post",
"path": "post",
"hidden": false,
"component": "system/post/index",
"meta": {
"title": "岗位管理",
"icon": "post",
"noCache": false,
"link": null
}
},
{
"name": "Dict",
"path": "dict",
"hidden": false,
"component": "system/dict/index",
"meta": {
"title": "字典管理",
"icon": "dict",
"noCache": false,
"link": null
}
},
{
"name": "Config",
"path": "config",
"hidden": false,
"component": "system/config/index",
"meta": {
"title": "参数设置",
"icon": "edit",
"noCache": false,
"link": null
}
},
{
"name": "Notice",
"path": "notice",
"hidden": false,
"component": "system/notice/index",
"meta": {
"title": "通知公告",
"icon": "message",
"noCache": false,
"link": null
}
},
{
"name": "Log",
"path": "log",
"hidden": false,
"redirect": "noRedirect",
"component": "ParentView",
"alwaysShow": true,
"meta": {
"title": "日志管理",
"icon": "log",
"noCache": false,
"link": null
},
"children": [
{
"name": "Operlog",
"path": "operlog",
"hidden": false,
"component": "monitor/operlog/index",
"meta": {
"title": "操作日志",
"icon": "form",
"noCache": false,
"link": null
}
},
{
"name": "Logininfor",
"path": "logininfor",
"hidden": false,
"component": "monitor/logininfor/index",
"meta": {
"title": "登录日志",
"icon": "logininfor",
"noCache": false,
"link": null
}
}
]
}
]
}
一个一级菜单
这种又细分为三种情况:
普通的菜单
这种普通菜单,点击之后,在右边打开一个新的选项卡,展示我们的系统内容。
菜单 JSON:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"path": "/",
"hidden": false,
"component": "Layout",
"children": [
{
"name": "Role",
"path": "role",
"hidden": false,
"component": "system/role/index",
"meta": {
"title": "角色管理",
"icon": "peoples",
"noCache": false,
"link": null
}
}
]
}
超链接(非外链)
对应到数据表中,is_frame 为 1 ,menu_type 为 M
菜单 JSON:
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
{
"name": "Http://www.yueyazhui.top",
"path": "/",
"hidden": false,
"component": "Layout",
"meta": {
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": null
},
"children": [
{
"name": "Www.yueyazhui.top",
"path": "www.yueyazhui.top",
"hidden": false,
"component": "InnerLink",
"meta": {
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": "http://www.yueyazhui.top"
}
}
]
}
超链接(外链)
菜单 JSON 有两种情况
一种情况,is_frame 为 1,但是 menu_type 为 C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"path": "/",
"hidden": false,
"component": "Layout",
"children": [
{
"name": "Http://www.yueyazhui.top",
"path": "http://www.yueyazhui.top",
"hidden": false,
"meta": {
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": "http://www.yueyazhui.top"
}
}
]
}
另一种情况,is_frame 为 0,menu_type 为 M:
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "Http://www.yueyazhui.top",
"path": "http://www.yueyazhui.top",
"hidden": false,
"component": "Layout",
"meta": {
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": "http://www.yueyazhui.top"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "Http://www.yueyazhui.top",
"path": "http://www.yueyazhui.top",
"hidden": false,
"component": "Layout",
"meta": {
"title": "TienChin健身官网",
"icon": "guide",
"noCache": false,
"link": "http://www.yueyazhui.top"
}
}
总结
- 如果 is_frame 为 0,说明这个菜单是一个外链,此时无论 menu_type 是 M 还是 C,都是在浏览器新的选项卡中打开外链。
- 如果 is_frame 为 1,说明不是一个外链(准确来说,是说这个菜单项的打开,不需要打开浏览器新的选项卡),菜单点击之后,是在我们自己的项目中打开一个新的选项卡,但是这个新的选项卡的展示有两种情况:
- 展示了一个外部网站的页面(此时,menu_type 为 M):超链接(非外链展示)
- 展示了项目中的一个菜单项(此时 menu_type 为 C):普通菜单(角色管理、有父有子中的子菜单)