这里用mysql 举例整理。我们常用的数据库例如 Oracle/ SQL Server 等,对于分页语法的支持大同小异。不做具体一一举例。
先从数据库层梳理,也是从最根源去分析分页的最终目的,前端和后端的一起逻辑和适配,都是为了拼接合适的 SQL 语句。
1.MySQL LIMIT
LIMIT row_count is equivalent to LIMIT 0, row_count.
参考:MySQL :: MySQL 5.7 Reference Manual :: 13.2.9 SELECT Statement
后端分页,简单讲,就是数据库的分页。对于mysql 来讲,就是上述 offset row_count 的计算过程。
这里选用了常用的框架组件来对比各自实现的细节。
pagehelper 是Java Orm 框架mybatis 常用的开源分页插件
spring-data-jdbc 是Java 框架常用的数据层组件
1.pagehelper
/*** 计算起止行号 offset* @see com.github.pagehelper.Page#calculateStartAndEndRow*/private void calculateStartAndEndRow() {// pageNum 页码,从1开始。pageNum < 1 , 忽略计算。this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);}
/*** 计算总页数 pages/ pageCount。* 在赋值数据总条数的同时,也计算了总页数。* 可以与 Math.ceil 实现对比看。*/public void setTotal(long total) {if (pageSize > 0) {pages = (int) (total / pageSize + ((total % pageSize == 0) ? 0 : 1));} else {pages = 0;}}
2.spring-data-jdbc
关键类:
org.springframework.data.domain.Pageable
org.springframework.data.web.PageableDefault
/*** offset 计算,不同于pagehelper, page 页码从0 开始。default is 0* @see org.springframework.data.domain.AbstractPageRequest#getOffset*/public long getOffset() {return (long)this.page * (long)this.size;}/** 总页数的计算使用 Math.ceil 实现。* @see org.springframework.data.domain.Page#getTotalPages()*/@Overridepublic int getTotalPages() {return getSize() == 0 ? 1 : (int) Math.ceil((double) total / (double) getSize());}
/*** offset 计算,不同于pagehelper, page 页码从0 开始。* @see org.springframework.data.jdbc.core.convert.SqlGenerator#applyPagination*/private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) {// 在spring-data-relation, Limit 抽象为 SelectLimitOffsetSelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select;// To read the first 20 rows from start use limitOffset(20, 0). to read the next 20 use limitOffset(20, 20).SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset());return (SelectBuilder.SelectOrdered) limitResult;}
/*** Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller method.** @see org.springframework.data.web.PageableDefault*/(RetentionPolicy.RUNTIME)(ElementType.PARAMETER)public PageableDefault {/*** The default-size the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding* parameter defined in request (default is 10).*/int size() default 10;/*** The default-pagenumber the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding* parameter defined in request (default is 0).*/int page() default 0;}
前端展示层,分别从服务端渲染方案以及纯前端脚本方案去看分页最终的页面呈现逻辑。
这里选取的分别是Java 常用的模板引擎 thymeleaf 以及热门的前端框架 element-ui。
从用法以及组件源码角度,去理清终端处理分页的常见方式。
1.thymeleaf - 模板引擎
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
<!-- spring-data-examples\web\example\src\main\resources\templates\users.html--><nav><!-- class样式 bootstrap 默认的分页用法--><ul class="pagination" th:with="total = ${users.totalPages}"><li th:if="${users.hasPrevious()}"><a th:href="@{/users(page=${users.previousPageable().pageNumber},size=${users.size})}" aria-label="Previous"><span aria-hidden="true">«</span></a></li><!-- spring-data-examples 分页计算从0 开始, /users?page=0&size=10 --><!-- 生成页码列表,呈现形式即我们常见的 ①②③④ --><li th:each="page : ${#numbers.sequence(0, total - 1)}"><a th:href="@{/users(page=${page},size=${users.size})}" th:text="${page + 1}">1</a></li><li th:if="${users.hasNext()}"><!-- 下一页实现,因为是服务器端渲染生成。在最终生成的html 中,这里的href 是固定的。实现思路和纯前端技术,使用javascript 脚本对比看 --><a th:href="@{/users(page=${users.nextPageable().pageNumber},size=${users.size})}" aria-label="Next"><span aria-hidden="true">»</span></a></li></ul></nav>
2.element-ui 前端框架
// from node_modules\element-ui\packages\pagination\src\pagination.js// page-count 总页数,total 和 page-count 设置任意一个就可以达到显示页码的功能;computed: {internalPageCount() {if (typeof this.total === 'number') {// 页数计算使用 Math.ceilreturn Math.max(1, Math.ceil(this.total / this.internalPageSize));} else if (typeof this.pageCount === 'number') {return Math.max(1, this.pageCount);}return null;}},/*** 起始页计算。page 页码从1 开始。*/getValidCurrentPage(value) {value = parseInt(value, 10);// 从源码的实现可以看到,一个稳定强大的开源框架,在容错、边界处理的严谨和思考。const havePageCount = typeof this.internalPageCount === 'number';let resetValue;if (!havePageCount) {if (isNaN(value) || value < 1) resetValue = 1;} else {// 强制赋值起始值 1if (value < 1) {resetValue = 1;} else if (value > this.internalPageCount) {// 数据越界,强制拉回到PageCountresetValue = this.internalPageCount;}}if (resetValue === undefined && isNaN(value)) {resetValue = 1;} else if (resetValue === 0) {resetValue = 1;}return resetValue === undefined ? value : resetValue;}
技术永远是关联的,思路永远是相似的,方案永远是相通的。单独的去分析某个技术或者原理,总是有边界和困惑存在。纵向拉伸,横向对比才能对技术方案有深刻的理解。在实战应用中,能灵活自如。
分页实现的方案最终是由数据库决定的,对于众多的数据库,通过SQL 语法的规范去框定,以及我们常用的各种组件或者插件去适配。
纵向对比,我们可以看到不同技术层的职责和通用适配的实现过程,对于我们日常的业务通用开发以及不同业务的兼容有很大的借鉴意义。
-end-

