129 分页功能实现详解 | RuoYi
- 前端采用基于
bootstrap
的轻量级表格插件bootstrap-table(opens new window) - 后端采用基于
mybatis
的轻量级分页插件pageHelper(opens new window)
提示:
前后端分页实现流程
一 前端
1 element-ui提供了el-pagination,可以直接去使用,是没问题的。
2 只是说项目里也封装了一个pagination组件,src\components\Pagination\index.vue。封装后的pagination组件兼容el-pagination,即el-pagination的所有属性、事件等等都会被支持。
<template><div :class="{'hidden':hidden}" class="pagination-container"><el-pagination:background="background":current-page.sync="currentPage":page-size.sync="pageSize":layout="layout":page-sizes="pageSizes":pager-count="pagerCount":total="total"v-bind="$attrs"@size-change="handleSizeChange"@current-change="handleCurrentChange"/></div>
</template><script>
import { scrollTo } from '@/utils/scroll-to'export default {name: 'Pagination',props: {total: {required: true,type: Number},page: {type: Number,default: 1},limit: {type: Number,default: 20},pageSizes: {type: Array,// 区别于el-pagination,一些默认值做了一些简单修改default() {return [10, 20, 30, 50]}},// 移动端页码按钮的数量,默认值5。// 如果非移动端则是7pagerCount: {type: Number,default: document.body.clientWidth < 992 ? 5 : 7},layout: {type: String,default: 'total, sizes, prev, pager, next, jumper'},background: {type: Boolean,default: true},autoScroll: {type: Boolean,default: true},hidden: {type: Boolean,default: false}},data() {return {};},computed: {currentPage: {get() {return this.page},set(val) {this.$emit('update:page', val)}},pageSize: {get() {return this.limit},set(val) {this.$emit('update:limit', val)}}},methods: {handleSizeChange(val) {if (this.currentPage * val > this.total) {this.currentPage = 1}this.$emit('pagination', { page: this.currentPage, limit: val })if (this.autoScroll) {// 点击下一页后,自动滚动到第1条数据所在的位置scrollTo(0, 800)}},handleCurrentChange(val) {this.$emit('pagination', { page: val, limit: this.pageSize })if (this.autoScroll) {scrollTo(0, 800)}}}
}
</script><style scoped>
.pagination-container {background: #fff;padding: 32px 16px;
}
.pagination-container.hidden {display: none;
}
</style>
3 全局注册封装的pagination组件(在前端任何页面都可以使用):main.js
// 全局组件挂载
Vue.component('Pagination', Pagination)
// 分页组件
import Pagination from "@/components/Pagination";
4 封装的pagination组件的使用案例:src\views\system\role\selectUser.vue
5 前端调用实现:前端定义分页流程
// 一般在查询参数中定义分页变量
queryParams: {pageNum: 1,pageSize: 10
},// 页面添加分页组件,传入分页变量
<paginationv-show="total>0":total="total":page.sync="queryParams.pageNum":limit.sync="queryParams.pageSize"@pagination="getList"
/>// 声明分页参数和查询参数
export default {data() {return {// 查询参数queryParams: {pageNum: 1, // 第1页pageSize: 10, // 每页10条roleId: undefined, // 查询条件1:roleId,角色iduserName: undefined, // 查询条件2:用户名称 phonenumber: undefined // 查询条件3:电话号码}};}
}// 调用后台方法,传入参数 获取结果
// queryParams是分页参数和查询参数
listUser(this.queryParams).then(response => {this.userList = response.rows;this.total = response.total;}
);
6 扩展:一般在项目里直接用封装的分页组件就已经足够满足了。如果不满足的话,可以再参考el-pagination去定制一些属性加进来。或者是你直接用饿了么的也可以啊,都是可以的。
二 后端
1 ruoyi-common-core#TableDataInfo:表格分页数据对象,其中rows就是表格数据
public class TableDataInfo implements Serializable
{private static final long serialVersionUID = 1L;/** 总记录数 */private long total;/** 列表数据 */private List<?> rows;/** 消息状态码 */private int code;/** 消息内容 */private String msg;
}
2 ruoyi-common-core#PageUtils:解决实体类(如下面的SysPost sysPost)中都没有pageSize和pageNumber属性接收前端参数
/*** 分页工具类* * @author ruoyi*/
public class PageUtils extends PageHelper
{/*** 设置请求分页数据:* 就是从request中把前端传过来的几个参数拿过来。* 然后设置到pageDomain里面。*/public static void startPage(){PageDomain pageDomain = TableSupport.buildPageRequest();Integer pageNum = pageDomain.getPageNum();Integer pageSize = pageDomain.getPageSize();String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());Boolean reasonable = pageDomain.getReasonable();// 调用分页插件PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);}
}
3 ruoyi-system#SysPostController#list
/*** 获取岗位列表*/@RequiresPermissions("system:post:list")@GetMapping("/list")public TableDataInfo list(SysPost post){// 分页startPage();// 当前页数据List<SysPost> list = postService.selectPostList(post);// 返回给前端return getDataTable(list);}
4 【pagehelper框架】PageMethod#startPage:分页
// 当前请求protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();// 即在当前请求下面,去给我们进行一个分页// 会自动在sql语句中拼接pageNum、pageSize、orderBy这3个参数的limit分页语句// 简化代码:不用在SysPostMapper.xml中修改sql语句public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {Page<E> page = startPage(pageNum, pageSize);page.setOrderBy(orderBy);return page;}
5 扩展:如果不想分页,就把startPage();这句代码删掉就行了。
三 注意事项(坑)
1 常见坑点1:selectPostById
莫名其妙的分页。例如下面这段代码
startPage();
List<User> list;
if(user != null){list = userService.selectUserList(user);
} else {list = new ArrayList<User>();
}
// 当user == null时,分页会消费在此查询上。
// 本来是查询单个的,但结果却莫名其妙给你分了个页
Post post = postService.selectPostById(1L);
return getDataTable(list);
原因分析:这种情况下由于user
存在null
的情况,就会导致pageHelper
生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。 当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子才能保证安全。
就是说你使用了这个startPage,你又不去分页啊,这个地方又没有分页。那么的话,你下一次查询就是下一个查询,它就会带上这个分页,所以的话呢就会莫名其妙。这个你本来不需要分页的,反而被分页了,所以的话就需要注意一下。
List<User> list;
if(user != null){startPage();list = userService.selectUserList(user);
} else {list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
2 常见坑点2:添加了startPage
方法。也没有正常分页。例如下面这段代码
startPage();
// 错:分页错误地加上这里
Post post = postService.selectPostById(1L);
List<User> list = userService.selectUserList(user);
return getDataTable(list);
原因分析:只对该语句以后的第一个查询(Select)
语句得到的数据进行分页。
上面这个代码,应该写成下面这个样子才能正常分页。
因为的话,你下面的话必须是你分页的那个sql语句的一个查询方法(XxxMapper.xml)。
Post post = postService.selectPostById(1L);
startPage();
List<User> list = userService.selectUserList(user);
return getDataTable(list);
3 提示
项目分页插件默认是Mysql
语法,如果项目改为其他数据库需修改配置application.yml
文件中的属性helperDialect: 你的数据库
4 注意
只要你可以保证在
PageHelper
方法调用后紧跟MyBatis
查询方法,这就是安全的。因为PageHelper
在finally
代码段中自动清除了ThreadLocal
存储的对象。 如果代码在进入Executor
前发生异常,就会导致线程不可用,这属于人为的Bug
(例如接口方法和XML
中的不匹配,导致找不到MappedStatement
时),这种情况由于线程不可用,也不会导致ThreadLocal
参数被错误的使用。