全栈角度看分页处理

京东云开发者
• 阅读 401

作者:京东物流 杨攀

分页是 web application 开发最常见的功能。在使用不同的框架和工具过程中,发现初始行/页的定义不同,特意整理记录。从这个技术点去看不同层的实现。以及不同语言实现的对比。
文章会从正常的web 结构分层的角度去梳理不同层的处理。
分为数据库分页、服务端分页、前端分页

数据库分页

这里用mysql 举例整理。我们常用的数据库例如 Oracle/ SQL Server 等,对于分页语法的支持大同小异。不做具体一一举例。
先从数据库层梳理,也是从最根源去分析分页的最终目的,前端和后端的一起逻辑和适配,都是为了拼接合适的 SQL 语句。

①MySQL LIMIT

语法:[LIMIT {[offset,] row_count}]

LIMIT row_count is equivalent to LIMIT 0, row_count.

The offset of the initial row is 0 (not 1)

参考:MySQL :: MySQL 5.7 Reference Manual :: 13.2.9 SELECT Statement

服务端/后端分页

后端分页,简单讲,就是数据库的分页。 对于mysql 来讲,就是上述 offset row_count 的计算过程。
这里选用了常用的框架组件来对比各自实现的细节。
pagehelper 是Java Orm 框架mybatis 常用的开源分页插件
spring-data-jdbc 是Java 框架常用的数据层组件

①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;
    }
}

SQL 拼接实现: com.github.pagehelper.dialect.helper.MySqlDialect

②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()
 */
@Override
public 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 抽象为 SelectLimitOffset 
    SelectBuilder.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;
}

spring-data-commons 提供 mvc 层的分页参数处理器

/**
 * Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller method. 
 *
 * @see org.springframework.data.web.PageableDefault
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface 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;
}

MVC 参数处理器: org.springframework.data.web.PageableHandlerMethodArgumentResolver

前端分页

前端展示层,分别从服务端渲染方案以及纯前端脚本方案去看分页最终的页面呈现逻辑。
这里选取的分别是Java 常用的模板引擎 thymeleaf 以及热门的前端框架 element-ui。
从用法以及组件源码角度,去理清终端处理分页的常见方式。

①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>

②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.ceil
      return 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 {
    // 强制赋值起始值 1
    if (value < 1) {
      resetValue = 1;
    } else if (value > this.internalPageCount) {
      // 数据越界,强制拉回到PageCount
      resetValue = this.internalPageCount;
    }
  }

  if (resetValue === undefined && isNaN(value)) {
    resetValue = 1;
  } else if (resetValue === 0) {
    resetValue = 1;
  }

  return resetValue === undefined ? value : resetValue;
},

总结

  • 技术永远是关联的,思路永远是相似的,方案永远是相通的。单独的去分析某个技术或者原理,总是有边界和困惑存在。纵向拉伸,横向对比才能对技术方案有深刻的理解。在实战应用中,能灵活自如。
  • 分页实现的方案最终是由数据库决定的,对于众多的数据库,通过SQL 语法的规范去框定,以及我们常用的各种组件或者插件去适配。
  • 纵向对比,我们可以看到不同技术层的职责和通用适配的实现过程,对于我们日常的业务通用开发以及不同业务的兼容有很大的借鉴意义。
  • 横向对比,例如前端展示层的实现思路,其实差别非常大。如果使用 thymeleaf,结构简单清晰,但交互响应上都会通过服务器。如果选用element-ui,分页只依赖展示层,和服务端彻底解构。在技术选型中可以根据各自的优缺点进行适度的抉择。
点赞
收藏
评论区
推荐文章
关于报表打印
1分页策略分页与打印时密切相关的,皕杰报表提供了四种分页策略,即按纸张大小分页、按数据行数分页、按数据列数分页、用户自定义分页和不分页。分页由2个因素来控制,一个每个页面的大小,另外一个是分页顺序(打印顺序)。打开或新建一张报表,单击报表的空白处,则与报表
宙哈哈 宙哈哈
1年前
使用验证码拦截爬虫和机器人实践分享
在很多时候我们都会遇到验证码的多种场景,不同的产品也会使用不同的登录验证方式。在项目开发中,我将KgCaptcha应用到搜索和分页中,下面是我写的的记录。
Wesley13 Wesley13
3年前
3. Vue
路由是根据不同的url地址展现不同的内容或页面。前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做(在单页面应用,大部分页面结构不变,只改变部分内容的使用),之前是通过服务器根据url的不同返回不同的页面。前端路由优点:用户体验好,不需要每次都从服务器全部获取,快速展现给用户缺点:不利于SEO,使用浏
Stella981 Stella981
3年前
SpringBoot学习之路:05.Spring Boot集成pagehelper分页插件
      前面说了SpringBoot集成持久层框架Mybatis的过程,和使用mybatis进行对数据库进行CRUD的操作,然而当对多数据进行查询时就需要进行分页了,分页技术分为客户端分页和服务器端分页(数据库分页),客户端分页是前端的数据插件对返回的数据集进行分页(bootstruptable、quitable等),客户端分页会对数据库和客
Stella981 Stella981
3年前
EntityFramework 6 分页模式
在我的另一篇博客中提到了EntityFrameworkCore分页问题(https://my.oschina.net/taadis/blog/890232),中提到了EntityFrameworkCore在针对不同版本_SQLServer_数据库时如何指定分页模式,那么如何在EntityFramework6中指定分页模式呢?场景重现
Stella981 Stella981
3年前
Mybatisplus实现在不分页时进行排序操作以及用分页接口实现全量查询
优化分页插件实现在不分页时进行排序操作原生mybatisplus分页与排序是绑定的,mpp优化了分页插件,使用MppPaginationInterceptor插件<br在不分页的情况下支持排序操作<brpage参数size设置为1可实现不分页取全量数据,同时设置OrderItem可以实现排序<br使用MppPaginationInt
Easter79 Easter79
3年前
SpringBoot学习之路:05.Spring Boot集成pagehelper分页插件
      前面说了SpringBoot集成持久层框架Mybatis的过程,和使用mybatis进行对数据库进行CRUD的操作,然而当对多数据进行查询时就需要进行分页了,分页技术分为客户端分页和服务器端分页(数据库分页),客户端分页是前端的数据插件对返回的数据集进行分页(bootstruptable、quitable等),客户端分页会对数据库和客
Stella981 Stella981
3年前
PAP 快速开发框架:mybatis
  背景:在使用mybatis的过程中,考虑到整合的框架在后期使用的过程中,有可能是需要兼容到多种数据库的,在这种前提条件下,完成通用CRUD功能的编写,本文前期先考虑到不同数据库针对分页功能的统一操作;例如mysql数据库的分页是limit关键字的使用,oracle数据库的分页是rownum关键字的使用;  demogit地址部分: h
Stella981 Stella981
3年前
JFinal各种场景(PC、APP、微信小程序等)分页方案
JFinal专题之分页解决方案【课程介绍】 详细介绍数据库分页原理,自己动手封装前端分页组件,然后介绍第三方的js分页组件,集成laypage插件,了解各种分页模式,不管是跳转分页,数据库分页、前端分页、滚动加载分页、ajax数据分页、APP移动端分页、微信小程序分页等【课程目标】 掌握数据库分页原理,熟练使用JFinal操作数据库分页查
研发日常踩坑-Mysql分页数据重复 | 京东云技术团队
踩坑描述:写分页查询接口,orderby和limit混用的时候,出现了排序的混乱情况在进行第N页查询时,出现与第一前面页码的数据一样的记录。问题在MySQL中分页查询,我们经常会用limit,如:limit(0,20)表示查询第一页的20条数据,limit