作者:京东物流 杨攀

分页是 web application 开发最常见的功用。在运用不同的结构和东西进程中,发现初始行/页的定义不同,特意收拾记载。从这个技能点去看不同层的完成。以及不同语言完成的比照。
文章会从正常的web 结构分层的视点去收拾不同层的处理。
分为数据库分页、服务端分页、前端分页

数据库分页

这儿用mysql 举例收拾。咱们常用的数据库例如 Oracle/ SQL Server 等,关于分页语法的支撑迥然不同。不做详细逐个举例。
先从数据库层收拾,也是从最根源去分析分页的最终目的,前端和后端的一同逻辑和适配,都是为了拼接适宜的 SQL 句子。

①MySQL LIMIT

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

LIMIT row_countis equivalent toLIMIT 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 – 模板引擎

Thymeleafis 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,分页只依靠展现层,和服务端彻底解构。在技能选型中能够根据各自的优缺点进行适度的抉择。