【RuoYi】数据分页功能分析 —— 以登录日志页面为例
本文基于 RuoYi-Vue v3.8.2,以"监控 → 登录日志"页面为例,从前端代码、前端开发者工具、后端代码到后端 Log 输出,完整分析 RuoYi 框架中数据分页的实现原理。
一、实例简介
本次分析选取的含数据分页功能的页面为:系统管理->日志管理->登录日志。
如图所示,页面底部有分页组件,显示"共 139 条"记录,每页显示 10 条,共 14 页。用户可以通过点击页码按钮来翻页查看数据,而非一次性加载全部 139 条记录。
二、为什么要数据分页?
以登录日志为例,假设系统运行多年,日志记录达到几十万条。若不分页,会带来以下问题:
| 层面 | 问题 |
|---|---|
| 前端显示 | 需渲染几十万行 DOM,页面严重卡顿甚至崩溃 |
| 前端内存 | 几十万条 JSON 数据全部存入内存,内存开销极大 |
| 传输速度 | 数据量过大,网络传输时间长,用户等待时间久 |
| 带宽 | 每次请求消耗大量带宽资源 |
| 后端内存 | 后端一次性从数据库查询全部数据,内存压力大 |
| 数据及时性 | 数据量大时查询慢,返回的数据可能已经过时 |
所以,分页是性能优化的必要手段,每次只查询当前页所需的少量数据,极大降低了前后端的资源消耗。
三、源码分析
3.1 前端代码
(1)pagination 分页组件
打开前端源码文件:ruoyi-ui/src/views/monitor/logininfor/index.vue
如图所示,在<template>末尾使用了<pagination>组件,关键属性如下:
<pagination v-show="total>0" //只有当后端返回的总条数大于 0 时,才显示分页组件 :total="total" //将后端返回的总条数传入组件,组件据此计算总页数 :page.sync="queryParams.pageNum" //双向绑定当前页码,用户点击翻页时自动更新 :limit.sync="queryParams.pageSize" //双向绑定每页条数,默认为 10 @pagination="getList" //每次翻页时触发getList方法重新请求数据 />同一文件中,queryParams的初始值定义如下:
queryParams: { pageNum: 1, // 默认第1页 pageSize: 10, // 默认每页10条 ipaddr: undefined, userName: undefined, status: undefined }total初始值为 0,需要等后端返回数据的总条数后才会赋值,分页组件才会显示出来。
(2)getList 函数调用链
getList方法是获取数据的核心函数,其调用链为:
getList→list()→request[axios]→ 后端接口
getList() { this.loading = true; list(this.addDateRange(this.queryParams, this.dateRange)).then(response => { this.list = response.rows; // 当前页的数据列表,赋值给表格展示 this.total = response.total; // 总条数,赋值给 total 后分页组件随即显示正确的总页数 this.loading = false; }); },打开 API 文件:ruoyi-ui/src/api/monitor/logininfor.js
其中list函数的完整代码如下:
// 查询登录日志列表 export function list(query) { return request({ url: '/monitor/logininfor/list', method: 'get', params: query // pageNum、pageSize 等参数作为 URL 查询参数发送 }) }list()函数通过 axios 发起 GET 请求,将pageNum、pageSize等参数拼接在 URL 后面发送给后端。
(3)前端开发者工具 —— 请求与响应
请求参数(Headers):
打开浏览器 F12 开发者工具,切换到 Network → Fetch/XHR,点击页面的第 2 页后,可以看到名为list?pageNum=2&pageSize=10...的请求:
Request URL:http://localhost/dev-api/monitor/logininfor/list?pageNum=2&pageSize=10
Request Method:GET
Status Code:200 OK
前端将pageNum=2、pageSize=10作为查询参数传递给了后端。
后端响应数据(Preview):
切换到 Preview 标签,可以看到后端返回的数据结构:
{
"total": 139, // 总条数,告诉前端共有多少条记录
"rows": [...], // 当前页的 10 条数据
"code": 200,
"msg": "查询成功"
}
total: 139被赋值给前端的this.total,分页组件据此显示"共 139 条"并计算出共 14 页;rows数组包含当前第 2 页的 10 条登录日志数据。
3.2 后端代码
打开后端 Controller 文件:ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java
其中负责分页查询的list方法如下:
@PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") @GetMapping("/list") public TableDataInfo list(SysLogininfor logininfor) { startPage(); // ① 读取分页参数 List<SysLogininfor> list = logininforService.selectLogininforList(logininfor); // ② 查询数据 return getDataTable(list); // ③ 封装返回结果 }后端实现分页只需两个关键函数:
| 函数 | 作用 |
|---|---|
startPage() | 从请求中读取前端传来的pageNum和pageSize,初始化 PageHelper 分页拦截器 |
getDataTable(list) | 将查询结果封装成包含total(总条数)和rows(当前页数据)的响应对象 |
四、分页实现原理分析
4.1 分页逻辑不在 Mapper 中实现
打开 Mapper 文件:ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml
selectLogininforList对应的 SQL 语句如下:
<select id="selectLogininforList" parameterType="SysLogininfor" resultMap="SysLogininforResult"> select info_id, user_name, ipaddr, login_location, browser, os, status, msg, login_time from sys_logininfor <where> <!-- 条件筛选 --> </where> order by info_id desc </select>可以看到,selectLogininforList对应的 SQL 语句中完全没有LIMIT关键字,分页逻辑并不在 Mapper 中实现。原因在于startPage()内部调用了PageHelper.startPage(pageNum, pageSize),PageHelper 作为 MyBatis 的拦截器,会在 SQL 执行前自动拦截并改写 SQL 语句,追加LIMIT子句,从而实现对查询结果的分页截取。
4.2 后端 Log 关键输出分析
点击登录日志第 2 页(pageNum=2&pageSize=10),查看 IDEA 控制台输出:
完整的 Log 内容如下:
# 第一条:PageHelper 自动生成的 count 查询,用于统计总条数返回给前端 21:01:43.064 [http-nio-8080-exec-41] DEBUG c.r.s.m.S.selectLogininforList_COUNT - [debug,137] - ==> Preparing: SELECT count(0) FROM sys_logininfor 21:01:43.065 [http-nio-8080-exec-41] DEBUG c.r.s.m.S.selectLogininforList_COUNT - [debug,137] - ==> Parameters: 21:01:43.067 [http-nio-8080-exec-41] DEBUG c.r.s.m.S.selectLogininforList_COUNT - [debug,137] - <== Total: 1 # 第二条:原查询被 PageHelper 拦截后自动加上 LIMIT,只取当前页数据 21:01:43.067 [http-nio-8080-exec-41] DEBUG c.r.s.m.S.selectLogininforList - [debug,137] - ==> Preparing: select info_id, user_name, ipaddr, login_location, browser, os, status, msg, login_time from sys_logininfor order by info_id desc LIMIT ?, ? 21:01:43.068 [http-nio-8080-exec-41] DEBUG c.r.s.m.S.selectLogininforList - [debug,137] - ==> Parameters: 10(Long), 10(Integer) # offset=10(跳过前10条),num=10(取10条) 21:01:43.069 [http-nio-8080-exec-41] DEBUG c.r.s.m.S.selectLogininforList - [debug,137] - <== Total: 10两条 SQL 的作用
PageHelper 将原本一条的查询语句自动拆分成了两条 SQL 执行:
第一条是selectLogininforList_COUNT,对应SELECT count(0) FROM sys_logininfor,用于统计表中所有符合条件的记录总数,结果即为返回给前端的total字段,分页组件据此计算总页数。
第二条是selectLogininforList,即原始查询语句被追加了LIMIT ?, ?后执行,只返回当前页的数据。
LIMIT 参数含义
两个占位符对应的参数为10(Long), 10(Integer),含义如下:
| 参数 | 值 | 含义 |
|---|---|---|
第一个? | 10(Long) | offset(偏移量):跳过前 10 条记录 |
第二个? | 10(Integer) | num(查询数量):取 10 条记录 |
LIMIT 值的计算公式:
LIMIT offset, num offset = (pageNum - 1) × pageSize = (2 - 1) × 10 = 10 num = pageSize = 10
即从第 11 条记录开始,取 10 条数据,恰好是第 2 页的内容。
推广验证:
若前端请求pageNum=3&pageSize=10,则:
offset = (3 - 1) × 10 = 20 num = 10 SQL: LIMIT 20, 10 → 跳过前 20 条,取第 21~30 条(第3页)
五、总结
| 层面 | 关键点 |
|---|---|
| 前端分页效果 | <pagination>组件负责显示分页条,绑定pageNum、pageSize、total |
| 前端请求 | getList→list()→ axios GET,将pageNum、pageSize作为参数传给后端 |
| 后端分页 | startPage()初始化 PageHelper,getDataTable()封装total和rows返回 |
| 分页原理 | PageHelper 拦截 SQL,自动执行两条语句:count(0)查总数 + 原查询加LIMIT |
| LIMIT 计算 | LIMIT (pageNum-1)*pageSize, pageSize,offset 控制起始位置,num 控制条数 |
RuoYi 框架通过 PageHelper 插件,让开发者无需手动写分页 SQL,只需在 Controller 中调用startPage()和getDataTable(),即可实现完整的分页功能,极大简化了开发工作。
