一、动态菜单路由
前端
我们发现动态路由的获取是在src\permission.js这个全局路由下的
//全局管理路由的,每个页面跳转的时候都要运行
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
其中的GenerateRoutes就是我们要寻找的对象,在src\store\modules\permission.js下我们可以找到对应实现的方法,
actions: {
// 生成路由
GenerateRoutes({ commit }) {
return new Promise(resolve => {
// 向后端请求路由数据
getRouters().then(res => {
const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(rewriteRoutes)
})
})
}
}
根据getRouters这个方法找到位于src\api\menu.js下的
// 获取路由
export const getRouters = () => {
return request({
url: '/getRouters',
method: 'get'
})
}
后端
通过查找getRouters这个方法我们找到位于SysLoginController.java中的
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
这个方法先是调用了SecurityUtils工具类中的获取ID,通过调用SecurityContextHolder.getContext().getAuthentication()获取已登陆的用户信息
建立一个List存放菜单,调用selectMenuTreeByUserId方法,我们找到接口的具体实现
@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);
}
通过用户ID将用户角色对应拥有的菜单全部查询出来,调用getChildPerms方法
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;
}
将传入的菜单进行遍历,使用迭代器将列表中的数据以此取出来,将它们的父ID和传入的parentId进行比较,一级菜单是没有parentId的所以数据库默认为0,将一开始的菜单列表和判断为一级菜单的数据传入recursionFn进行更一步比较
private void recursionFn(List<SysMenu> list, SysMenu t)
{
// 得到子节点列表
List<SysMenu> childList = getChildList(list, t);
t.setChildren(childList);
for (SysMenu tChild : childList)
{
if (hasChild(list, tChild))
{
recursionFn(list, tChild);
}
}
}
recursionFn先是将传递过来的数据传入getChildList方法中,得到二级菜单后,将childList 赋值给一级菜单,将二级菜单中的值一一和
列表通过hasChild方法,调用getChildList方法判断是否还有二级菜单后面的三级菜单,最后进行一个嵌套循环
private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t)
{
List<SysMenu> tlist = new ArrayList<SysMenu>();
Iterator<SysMenu> it = list.iterator();
while (it.hasNext())
{
SysMenu n = (SysMenu) it.next();
//判断n的父节点ID和t的ID是否相同
if (n.getParentId().longValue() == t.getMenuId().longValue())
{
tlist.add(n);
}
}
return tlist;
}
getChildList将传递过来的列表进行一个循环,在循环中将列表的值取出来,再将每个值的ParentId这个属性取出来和传入的一级菜单的ID进行一个对比,最后返回这个一级菜单的二级菜单tlist
在查到的所有的一级菜单,二级菜单等,我们还需将它构造成树的样子
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
若依使用了buildMenus来进行构建
buildMenus方法中第一段
for (SysMenu menu : menus)
{
RouterVo router = new RouterVo();
//是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现
router.setHidden("1".equals(menu.getVisible()));
//路由名字(menu中的路由地址)
router.setName(getRouteName(menu));
//路由地址(menu中的路由地址,将path前面加个/)
router.setPath(getRouterPath(menu));
//组件地址(menu中的组件路径)
router.setComponent(getComponent(menu));
//路由参数:如 {"id": 1, "name": "ry"}
router.setQuery(menu.getQuery());
//其他元素MetaVo
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
使用for循环将传递过来已经排好序的菜单列表进行循环,使用getRouteName方法,getRouterPath方法,getComponent方法,将其他元素放入MetaVo中
public String getRouteName(SysMenu menu)
{
//将获取到的Path首字母转化为大写
String routerName = StringUtils.capitalize(menu.getPath());
// 非外链并且是一级目录(类型为目录)
if (isMenuFrame(menu))
{
routerName = StringUtils.EMPTY;
}
return routerName;
}
getRouteName将获取到的Path首字母转化为大写,使用isMenuFrame来进行判断是否为目录
public boolean isMenuFrame(SysMenu menu)
{
return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType())
&& menu.getIsFrame().equals(UserConstants.NO_FRAME);
}
isMenuFrame通过父ID的值为0,菜单类型(菜单)(UserConstants.TYPE_MENU是若依定义的用户常量信息),是否菜单外链(否)
public String getRouterPath(SysMenu menu)
{
String routerPath = menu.getPath();
// 内链打开外网方式
if (menu.getParentId().intValue() != 0 && isInnerLink(menu))
{
routerPath = innerLinkReplaceEach(routerPath);
}
// 非外链并且是一级目录(类型为目录)
if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
&& UserConstants.NO_FRAME.equals(menu.getIsFrame()))
{
routerPath = "/" + menu.getPath();
}
// 非外链并且是一级目录(类型为菜单)
else if (isMenuFrame(menu))
{
routerPath = "/";
}
return routerPath;
}
getRouterPath方法获取路由地址,先使用了父ID和使用isInnerLink是否为内链组件进行判断,其次使用父ID,菜单类型(目录),是否菜单外链(否),最后再通过isMenuFrame进行判断
public String getComponent(SysMenu menu)
{
String component = UserConstants.LAYOUT;
if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu))
{
component = menu.getComponent();
}
else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu))
{
component = UserConstants.INNER_LINK;
}
else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu))
{
component = UserConstants.PARENT_VIEW;
}
return component;
}
getComponent,定义一个component默认为LAYOUT组件标识,先判断是否为空字符串和isMenuFrame方法判断是否为目录,将使用getComponent方法赋值给component更改为组件路径。
buildMenus第二段
List<SysMenu> cMenus = menu.getChildren();
if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()))
{
//当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
router.setAlwaysShow(true);
//重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
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);
当cMenus 不为空,且菜单类型为目录的时候嵌套循环,当找到菜单类型不是目录的时候使用if判断是否为菜单内部跳转,将不是目录的数据存入children,再存入列表中,再将列表放入一开始定义的router中,最后放入routers。