全棧角度看分頁處理

語言: CN / TW / HK

作者:京東物流 楊攀

分頁是 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.

```

```

②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,分頁只依賴展示層,和服務端徹底解構。在技術選型中可以根據各自的優缺點進行適度的抉擇。