路由
开源框架若依系统中,其前端菜单列表,即我们技术上通常所说的路由信息,是通过接口来返回给前端动态生成的。我们通过抓取接口发现,其接口路径为:getRouters。今天我们来通过接口代码,逐渐深入来研究若依框架中其菜单表设计结构。
getRouters
该接口位于ruoyi-admin模块controller包下的SysLoginController中,具体代码如下:
/** * 获取路由信息 * * @return 路由信息 */ @GetMapping("getRouters") public AjaxResult getRouters() { Long userId = SecurityUtils.getUserId(); List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId); return AjaxResult.success(menuService.buildMenus(menus)); }
代码第一行,是后台程序通过过滤器等来逐步获取用户的userId,这一步我们不多做阐述。
第二行,通过userId获取菜单树结构,我们来深入看一下其中的执行逻辑:
/** * 根据用户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,如果不是,则通过方法:selectMenuTreeByUserId来查询用户的菜单。我们暂且放一下最后一句话,先看这一句:
menus = menuMapper.selectMenuTreeByUserId(userId);
其对应的执行mysql语句为:
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time from sys_menu m left join sys_role_menu rm on m.menu_id = rm.menu_id left join sys_user_role ur on rm.role_id = ur.role_id left join sys_role ro on ur.role_id = ro.role_id left join sys_user u on ur.user_id = u.user_id where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0 order by m.parent_id, m.order_num
跟权限类似,其查询逻辑为:用户 -> 用户角色对应表->用户角色->角色菜单对应表->菜单。除一般的有效查询条件外(即state=1或者status=0,当然,如果要说的话,就是很奇怪为什么要把0设为正常,把1设为失效或停用),我们来看下面这一查询条件:
m.menu_type in ('M', 'C')
我们通过数据库,查看其数据表设计,其字段内容menu_type,如下:
即查询菜单的时候,只查询目录以及菜单,而不查询按钮类别。
如此,我们便把指定用户可以查看的所有目录,菜单都罗列了出来,反过来,我们再去看那一行代码:
getChildPerms(menus, 0);
深入方法内部:
/** * 根据父节点的ID获取所有子节点 * * @param list 分类表 * @param parentId 传入的父节点ID * @return String */ public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId) { List<SysMenu> returnList = new ArrayList<SysMenu>(); for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();) { SysMenu t = (SysMenu) iterator.next(); // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 if (t.getParentId() == parentId) { recursionFn(list, t); returnList.add(t); } } return returnList; }
不难看出,其实现主要是将菜单的链表list转换为一个“目录-菜单”的树状结构。
到这里结束了吗?不我们还有Controller中的最后一句没有看:
return AjaxResult.success(menuService.buildMenus(menus));
前面的AjaxResult.success(xxx)我们不阐述,其表示成功返回数据,我们展开方法buildMenus(menus)看一下:
/** * 构建前端路由所需要的菜单 * * @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("/inner"); List<RouterVo> childrenList = new ArrayList<RouterVo>(); RouterVo children = new RouterVo(); String routerPath = StringUtils.replaceEach(menu.getPath(), new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" }); 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; }
这里对菜单的判断主要有以下几种:
- 子菜单非空的“目录”
- 外链
- 路由地址为http开头的内链(即isInnerLink执行的判断)
如此,便成功的将存在数据库中的目录菜单,转换为了vue前端需要的路由地址映射结构,具体的,我们来看一下,前端的路由结构样式:
{ path: '', component: Layout, redirect: 'index', children: [ { path: 'index', component: (resolve) => require(['@/views/index'], resolve), name: 'Index', meta: { title: '首页', icon: 'dashboard', affix: true } } ] }