支持游标和偏移量的通用翻页机制

前言

对于大多数mobile App,当 App 发出请求时,通常不会在单个响应中收到该请求的全部结果,而是以分片的方式获取部分结果。

随着业务需求的变化,某些情况下 App 的翻页机制可能会调整。一般我们通过 App 重新发版,服务端和客户端同步调整分页机制来完成调整。而本文提供了一种通用协议,支持仅通过服务端发版来调整 App 的分页机制。

常见分页机制

基于游标的分页

游标是指标记数据列表中特定项目的一个随机字符串。该项目未被删除时,游标将始终指向列表的相同部分,项目被删除时游标即失效。因此,客户端应用不应存储任何旧的游标,也不能假定它们仍然有效。

Request的结构一般如下:

1
2
https://api.sample.com/v3/users/?limit=30&before=NDMyNzQyODI3OTQw
https://api.sample.com/v3/users/?limit=30&after=MTAxNTExOTQ1MjAwNzI5NDE=

参数说明:

  • limit:每个页面返回的单独对象的数量。请注意这是上限,如果数据列表中没有足够的剩余对象,那么返回的数量将小于这个数。为防止客户端传入过大的值,某些列表的 limit 值将设置上限。
  • before:向后查询的起始位置。
  • after:向前查询的起始位置。

Response结构一般如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"rows": [
... Endpoint data is here
],
"paging": {
"cursors": {
"top": "MTAxNTExOTQ1MjAwNzI5NDE=",
"last": "NDMyNzQyODI3OTQw"
},
"previous": "NDMyNzQyODI3OTQw",
"next": "MTAxNTExOTQ1MjAwNzI5NDE="
}
}

参数说明:

  • rows:如果当前页没有数据,或者根据过滤规则(比如隐私)当前页所有数据都被过滤掉,返回空的数组。客户端程序不能根据rows是否为空数组来判断,是否已经滚动到列表的末尾,而应根据下面的next字段是否有值来决定是否滚动到了列表尾部。
  • top:已返回的数据页面开头的游标。
  • last:已返回的数据页面末尾的游标。
  • previous:上一页数据的 API 端点。如果是null或者没有此字段,则表示返回的是第一页数据。
  • next:下一页数据的 API 端点。如果是null或者没有此字段,则表示返回的是最后一页数据。

基于偏移量的分页

Request的结构一般如下:

1
https://api.sample.com/v3/users/?limit=30&offset=30

参数说明:

  • limit:每个页面返回的单独对象的数量。
  • offset:偏移量,查询的起始位置。

一般情况下,还会包含其他查询条件,比如根据关键字查找姓名和关键字匹配的用户

Response结构一般如下:

1
2
3
4
5
6
{
"rows": [
... Endpoint data is here
],
"count": 10765
}

参数说明:

  • count:符合查询条件的总记录数。客户端根据offsetcount判断是否已经滚动到列表尾部。

注意,如果正分页的项目列表添加了新的对象,后续基于偏移量的查询的内容都将发生更改。

支持游标和偏移量的通用分页机制

每个API视场景需要实现部分规范(比如仅实现向后翻页,不实现向前翻页),没有实现的行为统一返回一个特定错误码 “not supported”

HTTP Request

  • limit: 必填项;期望返回的记录数量;整数类型;必须大于等于0
  • after: 可选项;字符串类型;表示查询从after指向的记录之后(不包括after指向的当前记录)的limit条记录
  • before: 可选项;字符串类型;表示查询从before指向的记录之前(不包括before指向的当前记录)的limit条记录
  • 备注:
    • Request中beforeafter不能并存
    • 如果Request中没有beforeafter,视为从结果集起始位置向后查询
    • MySQL和MongoDB均不支持基于游标位置的向前查询,如需支持需要在程序逻辑中实现

HTTP Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"code": 0,
"result": {
"rows": [
//"... Endpoint data is here"
],
"paging": {
"cursors": {
"top": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19",
"last": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19"
},
"previous": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19",
"next": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19",
"count": 1087
}
}
}
  • paging.cursors.top: 必填项;字符串或者null;指向已返回的数据页面开头的游标
  • paging.cursors.last: 必填项;字符串或者null;指向已返回的数据页面末尾的游标
  • paging.previous: 必填项;字符串或者null;查询前一页数据的末尾位置,Request中将此值赋给before,为null时,表示没有前一页
  • paging.next: 必填项;字符串或者null;查询后一页数据的起始位置,Request中将此值赋给after,为null时,表示没有后一页
  • paging.count: 可选项;整型;结果集总数
  • 备注:
    • rows为空结果集时:paging.cursors.toppaging.cursors.lastnullpaging.previouspaging.next不一定为null
    • 仅一条结果集是,paging.cursors.toppaging.cursors.last相同

参考