/ 博客列表 / 一个跨服务分页示例

一个跨服务分页示例

把一个(潜在)长的数据列表渲染到 UI 中时,通常会进行分页展示。分页展示每次只展示全部数据的一部分。 如果希望展示更多的数据,那就要求用户需要进行额外的操作。

常见的分页大致有两种模式:

  • 传统分页:通常会有一个页数页码导航条在 UI 的下方,显示当前是第几页,总共有多少页,甚至还会显示有多少数据。
  • 无限下拉:通常是首先展示前面的一组数据,然后用通过点击下方的“加载更多”按钮,或者在触摸设备屏幕上用手指上划, 或者页面检测到用户看到最后几条数据后自动加载,来加载更多数据。新加载的数据通常放在已显示数据的后面。

传统分页

前端示例

下面是一个可以交互的传统分页的例子:

名字性别元素属性地区
0

后端实现

如果数据被存放在关系型数据库中,那么,查询总页数/总数只需要一个 count, 查询当前页数据只需要一个 limit 就可以做到了。

查询 API 可能长这样:

GET /api/v1/playable-characters?pageNo=${pageNo}&pageSize=${pageSize}

- pageNo:   页号,从 1 开始
- pageSize: 每页大小,至少为 1,最大为 100

返回数据示例:

{
  "pageNo":     1,  // 本页页号
  "pageSize":   3,  // 每页大小
  "pageCount":  6,  // 总共多少页
  "totalCount": 17, // 总共多少数据
  "data":[
    { "id": 100, "name": "钟离", ...  },
    ...
  ]
}

查询的 SQL 可能长这样:

-- 查询总数
select count(*) from playable_characters;

-- 查询本页数据
select * from playable_characters
order by id desc
limit ${(pageNo - 1) * pageSize}, ${pageSize}

然后根据 SQL 查询结果,来计算总页数之类的数据。

优点

  • 能够在分页条的位置展示总数据量。
  • 适合 PC 端。这种方式在企业应用中非常常见。
  • 用户可以自己去改变分页大小,以便在一屏中展示更多数据。

缺点

这里的问题在于:

  • 当数据量较大时,countlimit 可能消耗的数据库的计算资源稍多一点。
  • 因为通常大家都是按时间逆序排列,所以在翻页时,或者修改本页数据并刷新本页数据时,会出现数据被往后挤的情况。 不过大家都接受这种情况。
  • 如果遇到数据删除,在往最后一页翻页时,可能会出现最后一页没有数据的情况。这时需要后端和前端开发人员注意处理一下。

无限下拉

前端示例

下面是一个通过点击”加载更多“按钮进行无限下拉的分页示例:

名字性别元素属性地区
示例已到底了。再体验一次?

后端实现

如果数据被存放在关系型数据库中,那么通常来说,只需要按递增的 ID 往前查即可。相比于传统分页, 这种方式不需要查询总页数/总数,而且是一个范围查询,查询效率会传统分页高一些。如果没有递增 ID, 那么通常也可通过创建时间+主键/唯一键的方式来排序。

查询 API 可能长这样:

GET /api/v1/playable-characters?offset=${offset}&size=${size}

- offset: 偏移量。这里填入一个数据的 id,查询此数据之前的数据。缺省时查询最新数据
- size:   从 offset 往前查 size 个数据

返回数据示例:
[
  { "id": 100, "name": "钟离", ...  },
  ...
]

- 前端在使用此 API 查询第一页数据时,offset 不设。
- 查询下一页的话,就把前一页的最后一条数据的 id 用作下一页的 offset。

查询 SQL 更加简单,只有一条:

select * from playable_characters
where id < ${offset}
order by id desc limit ${size}

前端需要判断翻页是否到底了,有几种办法:

  1. 如果查询某一页时,返回数据数量小于 size,或者直接返回个空数组,则认为是到底了。 这样的话,如果最后一页恰好返回的是空数组,那么就会出现加载更多时,确什么都没有加载到的情况。 如果不吹毛求疵的话,这种做法是没问题的。
  2. 前端每次都查询 size + 1 条数据,但是展示 size 条数据。 如果也返回了 size + 1 条数据,那么就是还有下一页(下一页至少有一条数据)。
  3. 后端直接告诉前端是否还有更多数据。这时要修改返回数据格式,比如成这样:
    {
      "hasMore": true,  // 是否还有下一页
      "data":[
        { "id": 100, "name": "钟离", ...  },
        ...
      ]
    }
    后端计算 hasMore 的方式,也是每次查询 size + 1 条数据。

优点

  • 查询 SQL 减少了一个,而且更加简单。
  • 较为适合移动端。

缺点

  • 使用者没法估计数据总量。
  • 使用者没法设置翻页大小(移动端也没人在意这种事)
  • 发生数据删除时,判断是否有下一页的做法可能是不准的。这个也没办法。大家应该都能接受这种情况。
  • 前端需要额外处理加载最新数据的逻辑。要么是加载最近数据,把它们展示在列表的前面,要么就毁掉全部已加载数据,再来一遍。

更复杂的情况

在前端展示的同一个列表中的数据来自两张不同的表

这时可以简单对这两张表做 union。这时,传统分页和无限分页的模式都还适用,只是后端写的查询可能会复杂一点。

在前端展示的同一个列表中的数据来自两个不同的子系统

这里有一个例子。见:TODO: