Ruoyi-vue项目讲解

@[TOC]若依前后端调用接口解读
若依github官方下载地址
若依gitee官方下载地址

1.验证码时候的前端调用接口

调用前端登录界面的时候,调用的是login.vue这个文件中的created函数

这里我们查看getCode函数方法

可以看到,这里先调用了一个getCodeImg函数,然后接收到后端返回的值之后,再进行相应的处理,显示图片以及保存redis中的key,进入getCodeImg函数进行查看

到api/login.js的文件下面查看getCodeImg函数

可以看出,这里再次调用了一个request函数,查看request内容

复制代码
import request from '@/utils/request'

查看request.js的文件

这里的process.env.VUE_APP_BASE_API我们通过.env.development查看定义

可以看出,这里每次前端发出api的时候都会添加固定的前缀/dev-api前缀。

反向代理

除此之外,这里的url还调用了一次反向代理,在vue.config.js之中配置

可以看出来这里跨域发送到后端的时候将process.env.VUE_APP_BASE_API转换为空字符串,并且将发送端口转为8080端口,通过浏览器我们也可以观察到前端发送的url请求内容

2.验证码后端调用接口

发送完成之后,跨域转化完成之后就是localhost:8080/captchaImage接口,找到后端对应的调用部分,首先从数据库中查询是否开启验证码

接下来保存验证码在redis中保存的信息并且生成验证码,生成的字符串类似于4+1=?@5这种形式

这里将4+1=?@5这块字符串拆分开来,将4+1生成图片,5保存在redis中为答案

将后端的结果返回到前端,前端保存图片内容以及redis的值

这里我们可以看到this.codeUrl对应图片显示的部分

对应的验证码部分图片如图所示

前端接收到后端返回的图片之后,成功显示图片

3.前端发起登录请求的调用

点击登录之后,从浏览器中查看发起的请求

从上面可以看出,login、getInfo和getRouters三个请求在点击登录按钮之后同时被发送出去,我们首先看登录按钮调用的方法

从这里可以看出,登录按钮调用handleLogin方法,查看方法的实现

这里首先查看是否需要记住密码,需要的时候保存在cookie之中,接下来会发生跳转

javascript 复制代码
this.$store.dispatch("Login", this.loginForm).then(() => {...})

这里跳转到src/store/modules/user.js中的Login函数之中

这里又调用了login函数,查看login函数的部分

发出一个post请求,将username、password、code、uuid发送出去

4.后端接收请求内容

后端接收请求为四个变量,这里后端将四个变量打包成一个结构体,然后进入到loginService.login函数中进行查看

首先进行验证码的验证,进入validateCaptcha函数中

这里通过从redis中取出验证码数据来与自己输入的数据进行判断,如果验证码过期或者不相同的情况下抛出各种异常,而如果验证码通过的时候继续通过用户名和密码进行验证

这里的authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,password));

就调用了UserDetailsService的实现类UserDetailsServiceImpl中的loadUserByUsername这个函数从数据库中取出来用户名和密码并且封装到LoginUser类之中

最终如果验证成功的情况下,从authentication中取出LoginUser类,然后记录用户的登录信息并且生成token内容

5.前端发送/getInfo请求的调用

这两个调用确实比较难找,在permission.js文件之中调用beforeEach函数的时候出现

这里的beforeEach函数在跳转页面的时候进行调用

我们首先查看GetInfo函数的调用,GetInfo在store/modules/user.js之中

可以看出这里调用了getInfo方法,查找此方法

这里的getInfo方法调用通过get发送了一个url的请求,该请求获取当前用户的信息并使用全局存储一下,接下来我们查看后端对于/getInfo请求的响应

这里查询用户以及权限信息返回给前端部分,然后我们继续查看前端接收到返回值之后的调用

可以看到GetInfo拿到角色权限等相关信息之后,才能够查找到可访问的路由表

6.前端发送/getRouter请求的调用

查看GenerateRoutes的调用

这里查看调用的GenerateRoutes发出的请求,在src/store/modules/permission.js之中(实在找不到就采用全局搜索的方式)。

查看getRouters方法的调用

javascript 复制代码
import { getRouters } from '@/api/menu'

可以看出这里通过get发送/getRouters的url请求,回到后端查看getRouters方法的调用

7.后端接收getRouters请求

这里采用的是一个不断延伸的树,进入到buildMenus函数之中,首先查看selectMenuTreeByUserId函数

查看getChildPerms函数的调用,这一步是建树的关键

可以看出根节点为0,因此传入的parentId为0,进入到getChildPerms函数之中

这里可以看出,如果遍历节点的根节点为当前节点的时候,就继续深入遍历,然后将遍历的节点放入当前节点的list之中,这样就建立了一个不断深入的递归树。
getChildPerms只是进行了第一层,将指定的父节点与孩子节点建立联系,接下来继续深入需要递归调用,进入recursionFn函数的调用环节

java 复制代码
private void recursionFn(List<SysMenu> list, SysMenu t)
{
    // 当前节点的子节点放入list之中
    List<SysMenu> childList = getChildList(list, t);
    //找一级菜单的子菜单
    t.setChildren(childList);
    for (SysMenu tChild : childList)
    //继续遍历孩子节点,二级菜单里面找三级菜单
    {
        if (hasChild(list, tChild))
        {
            recursionFn(list, tChild);
        }
    }
}

/**
 * 得到子节点列表
 *
 *
 * 从23条数据中把系统的子菜单找到
 */
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();
        if (n.getParentId().longValue() == t.getMenuId().longValue())
        //找到当前节点的子节点并放入tlist中,n.getParentId()为当前的孩子节点,
        //t.getMenuId()为遍历的23个节点Id
        {
            tlist.add(n);
        }
    }
    return tlist;
}

接下来,再使用getRouters方法将buildMenus方法打包

java 复制代码
@Override
public List<RouterVo> buildMenus(List<SysMenu> menus)
    ......

将List< SysMenu >转换为List< RouterVo>的类型,然后返回给前端,前端接收到之后进行处理

8.登录之后的跳转界面发送getList请求方法

接下来我们需要看登录成功之后页面如何跳转的,回到login.vue之中

可以看出登录成功之后往根路径去跳转

java 复制代码
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});

此时我们需要知道根路径"/"指的是哪个界面,打开src/router/index.js之中去查看

这里我们找到了跟路径对应的界面,进入到''之后我们会自动地往index路径去跳转,打开@/views/index查看页面,发现这里就是首页的各种内容。

并且这个首页还调用了Layout组件的布局

这里我们查看主页侧边栏的调用

从这里可以看到调用侧边栏sidebar-container,具体的Sidebar调用查看import内容

java 复制代码
import {AppMain, Navbar, Settings, Sidebar, TagsView } from './components'

具体的内容在src/layout/components/Sidebar之中有index.vue,这里sidebar-item之中有v-for循环

html 复制代码
 <sidebar-item
     v-for="(route, index) in sidebarRouters"
     :key="route.path  + index"
     :item="route"
     :base-path="route.path"
 />

这里遍历的是我们的router对象,可以把后台的数据迭代出来。对应路径在src/views/system/,这里跳转的方向在数据库中有所记录

比如第一个用户管理对应的路径在views之中的system/user/index.vue

点击侧边栏进入用户管理,发现页面分为两块

一块是左边的树状侧边栏,另外一块是中间的查询结果显示,中间的数据属于list,而侧边栏的数据属于tree,现在需要读取这两块的数据。在views/system/user/index中去找created方法,发现正好出现了这两部分的内容

进入到getList()函数中查询用户列表

java 复制代码
getList(){
  this.loading = true;
  //让前端页面有一个加速转圈圈的效果
  listUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
      //查看listUser代码的入口
      //取到这两个变量之后view会自动加载相应的效果???
      this.userList = response.rows;
      this.total = response.total;
      //获取到了list和total数据
      this.loading = false;
   }
}

查看listUser函数的调用

javascript 复制代码
// 查询用户列表
// 这里找到了listUser调用的请求方法
export function listUser(query) {
  return request({
    url: '/system/user/list',
    method: 'get',
    params: query
  })
}

9.后端处理/system/user/list方法

这里由于/system/user/list后端采用的是分层url的处理方式,因此我们先搜索list

这里先查看@ss.hasPermi('system:dept:list')")的权限调用方法

这里判断用户是否具有某种权限

这里取出LoginUser之后采用的是切分的方式判断是否具有权限,查看hasPermissions函数的调用

java 复制代码
private boolean hasPermissions(Set<String> permissions, String permission)
{
    return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}

这里permissions = * : * : * ,而permission = "system:user:list",判断权限的时候如果为全权限 或者permissions字符串中包含permission,则可以获取到权限。

查看startPage函数调用,

java 复制代码
protected void startPage()
{
    PageDomain pageDomain = TableSupport.buildPageRequest();
    //pageDomain一个新对象,专门用来获取分页信息的
    Integer pageNum = pageDomain.getPageNum();
    Integer pageSize = pageDomain.getPageSize();
    if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
    {
        String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
        Boolean reasonable = pageDomain.getReasonable();
        PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
        //PageHelper可以单独设置页数,把分页参数设置到PageHelper提供的对象中即可
        //reasonable对参数进行逻辑处理,保证参数的正确性,比如pageNum=0或-1,将自动将pageNum设置为1
    }
}

这里定义了PageDomain等参数都是为了从前端发回的请求取出pageNum和pageSize这两个参数的,orderBy用来定义排序的顺序,reasonable对参数进行逻辑处理,保证参数的正确性,最后调用PageHelper中startPage方法封装好参数,调用PageHelper中的startPage会保证下一条sql语句进行分页处理,并且将结果封装成Page类型对象。

然后调用查询语句查询,并且将结果返回给前端。

前端取回到数据之后,就会自动加载界面

10.登录之后点击用户管理发出的第二次请求

登录之后点击用户管理之后,在src/views/system/user/index.vue之中查看created函数的调用,在getList之后会发出第二次请求getTreeselect获取侧边栏的显示图

查看treeselect函数的调用

找到treeselect函数发出的请求,这里为部门下拉树结构

javascript 复制代码
export function treeselect() {
  return request({
    url: '/system/dept/treeselect',
    method: 'get'
  })
}

找到获取部门下拉树的后端调用

这里selectDeptList选择出部门信息的表格,deptService.buildDeptTreeSelect(depts)将depts建成一层一层的树,查看其中的代码

java 复制代码
@Override
public List<TreeSelect> buildDeptTreeSelect(List<SysDept> depts)
{
    List<SysDept> deptTrees = buildDeptTree(depts);
    //将10条记录组装成一个树状图
    return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
    //将deptTrees全部封装成TreeSelect,然后封装成集合,从SysDept封装成TreeSelect
    //需要转换的原因:SysDept中的字段过多,而TreeSelect中的字段较少,前端不需要SysDept
    //一样这么多的字段,本质上就是对字段进行复制
    //这里通过构造函数进行映射
    /***
     * public TreeSelect(SysDept dept)
     * {
     *    this.id = dept.getDeptId();
     *    this.label = dept.getDeptName();
     *    this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
     * }
     */
 }

这里buildDeptTree针对depts建立多层树,而由于SysDept类中包含了过多的内容,因此需要将SysDept转换成TreeSelect类,初始化类为

java 复制代码
public class SysDept extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 部门ID */
    private Long deptId;

    /** 父部门ID */
    private Long parentId;

    /** 祖级列表 */
    private String ancestors;

    /** 部门名称 */
    private String deptName;

    /** 显示顺序 */
    private String orderNum;

    /** 负责人 */
    private String leader;

    /** 联系电话 */
    private String phone;

    /** 邮箱 */
    private String email;

    /** 部门状态:0正常,1停用 */
    private String status;

    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;

    /** 父部门名称 */
    private String parentName;
    ... 
 }

接着查看TreeSelect类调用

java 复制代码
public class TreeSelect implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** 节点ID */
    private Long id;

    /** 节点名称 */
    private String label;
    ......
}

也就是说前端不需要这么多的信息,所以选择TreeSelect这么多的内容即可

这里SysDept类的初始化方法为

java 复制代码
public TreeSelect(SysDept dept)
{
    this.id = dept.getDeptId();
    this.label = dept.getDeptName();
    this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
}

点击研发部门之后的内容

点击研发部门之后基于研发部门进行进一步的查询

在src/views/system/user/index.vue之中查看el-tree标签内容

查看handleNodeClick的方法

这里查看getList方法调用

java 复制代码
getList() {
  this.loading = true;
  //让它有一个加载的效果,转圈圈
  listUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
      //查看listUser代码的入口
      //取到这两个变量之后view会自动加载相应的效果???
      this.userList = response.rows;
      this.total = response.total;
      //获取到了list和total数据
      this.loading = false;
    }
  );
},

this.loading = true让它加载有一个转圈圈的功能,而listUser调用相应的请求

javascript 复制代码
export function listUser(query) {
  return request({
    url: '/system/user/list',
    method: 'get',
    params: query
  })
}

这里调用/system/user/list与之前的区别在于这里的user加入了其他的参数,因此查找的时候是条件查找。

文件的导入和导出

这里有文件的导入和导出两个按钮,导入是将文件的数据写入到数据库中,而导出则是将数据库的数据写出到文件中

前端点击新增方法

提交按钮的方法

点这个能把所有的断点都去掉。

异步任务管理器

异步任务管理器中的调用在ruoyi-framework中的src/main/java/com.ruoyi.framework.web.service.SysLoginService.java中的

java 复制代码
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));

1.调用异步管理器

java 复制代码
AsyncManager.me()

获取一个AsyncManager对象,AsyncManager是一个单例模式

2.执行execute方法,执行任务,传入的是一个Task对象,实现了Runnable接口,表示它是一个任务,由线程Thread去执行。

封装了登录信息,执行了添加操作,这里不会执行,而是交给线程对象来执行。

这里首先调用了异步线程池

java 复制代码
/**
* 异步操作任务调度线程池
*/
private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
......
/**
 * 执行任务
 * 
 * @param task 任务
 */
public void execute(TimerTask task)
{
    executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}
/***
* 这里是创建线程池的操作,corePoolSize为核心池的大小
* 异步任务管理器内部定义了一个线程池,然后根据业务创建
* 添加日志的任务,交给线程池来处理,这样做到来日志和业务
* 的抽象、解耦合,日志全部统一处理。
* 同步:登录成功之后必须记录日志,如果没有记录下来登录成功
* 这部分就需要等
***/
相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试