Appearance
分页、过滤与排序
本章导读:列表接口是 API 流量最大的入口,三件套(分页/过滤/排序)的参数设计直接决定可用性与性能。本章对比偏移分页与游标分页,给出参数命名、运算符设计、
Link头标准化方案与深分页的工程解法。
1. 集合响应的外壳
所有列表端点复用同一个信封(见响应结构与命名):
jsonc
GET /v1/articles?status=published&sort=-createdAt&page=2&pageSize=20
{
"items": [ /* … */ ],
"page": 2,
"pageSize": 20,
"total": 214, // 可选:总数(深分页/大表下可能昂贵,允许为 null)
"links": { // 可选:HATEOAS 风格;或用 RFC 8288 Link 响应头
"next": "/v1/articles?status=published&sort=-createdAt&page=3&pageSize=20",
"prev": "/v1/articles?status=published&sort=-createdAt&page=1&pageSize=20"
}
}http
// 等价的头部表达(RFC 8288,GitHub/Google JSON:API 均采用)
Link: <...&page=3>; rel="next", <...&page=1>; rel="prev"2. 分页模型选择
2.1 偏移分页(page/pageSize 或 offset/limit)
GET /articles?page=2&pageSize=20 ← 页码式
GET /articles?offset=20&limit=20 ← 偏移式(JSON:API 风格)- 优点:实现直白(SQL
LIMIT/OFFSET)、可随机跳页、能显示"共 N 页"。 - 缺点:
- 数据漂移:翻页间隙有新数据插入 → 条目重复或漏显;
- 深分页性能悬崖:
OFFSET 100000需要扫过并丢弃十万行。
适用:后台管理、中小数据量、需要跳页的 UI。
2.2 游标分页(cursor-based)
GET /articles?sort=-createdAt&limit=20 ← 第一页
GET /articles?sort=-createdAt&limit=20&cursor=eyJvIjoiMjAyNi0wOS0xOFQwODozMDowMFoifQ游标是服务端编码的不透明指针(内部含排序键 + 唯一 ID),客户端不必理解。
- 优点:结果稳定(快照语义)、性能平坦(
WHERE (createdAt, id) < (?, ?) ORDER BY … LIMIT 20走索引)。 - 缺点:不能随机跳页;要求排序键唯一(不唯一时追加
id作 tie-breaker)。
适用:信息流(时间线 feed)、无限滚动、高并发大表。社交 App 的"下拉刷新加载更多"全是游标分页。
2.3 决策表
| 需求 | 选择 |
|---|---|
| 页码导航、总页数展示 | 偏移分页 |
| 无限滚动、消息流 | 游标分页 |
| 数据量 > 百万且写频繁 | 游标分页(或限制最大偏移 maxOffset=10000) |
| 导出/遍历全量 | 游标 + 作业资源,禁止超深偏移 |
3. 过滤参数设计
3.1 一等公民规则
- 参数名 = 资源字段名(
status=published),保持与 JSON 表示一致; - 未识别的参数必须拒绝或告警(
400+ problem details)——静默忽略会导致客户端误以为过滤生效; - 可过滤字段要文档化白名单,不默认全字段开放(防慢查询与探测)。
3.2 常用运算符模式
# 等值 / 多值
?status=published&tag=rest,http
# 比较(概念写法;实际需百分号编码 > <,或改用命名式)
?createdAtGte=2026-09-01T00:00:00Z&viewsLte=1000
# 表达式式过滤(Google AIP-160 风格:单参数 + 字符串表达式)
?filter=createdAt >= "2026-09-01T00:00:00Z" AND views > 1000
# 前缀/模糊
?q=REST&searchFields=title
# 空值
?deletedAt=null
# 组合(AND 默认;OR 显式)
?status=published&authorId=7另一流派是"后缀运算符"(createdAtGt=…,Microsoft 风格)或"中缀"(createdAt[gt]=…,Laravel/JSON:API 常见方括号式)。没有对错,只有统一——选定一种写进 OpenAPI。
3.3 范围与时间的坑
- 时间区间统一左闭右开:
?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z(含 9 月整月),文档写明; - 日期参数一律 RFC 3339 或
YYYY-MM-DD,拒绝本地格式; - 数值区间用
min/max或gt/ge/lt/le,一套到底。
4. 排序参数设计
?sort=createdAt ← 单字段升序
?sort=-createdAt ← 减号前缀 = 降序(GitHub/JSON:API 通用惯例)
?sort=-views,createdAt ← 多字段:按优先级排列
?sortOrder=desc ← 与 sortField=title 成对出现(另一流派)规则:
- 默认必须有稳定排序(如
-createdAt,id)——否则翻页会重复/漏数据; - 可排序字段白名单化(排序列 = 索引列,否则大表
ORDER BY会炸库); - 游标分页的
cursor必须与sort绑定:换排序即失效(服务端校验,不匹配返回 400)。
5. 字段选择与关联扩展(缓解 over-fetching)
GET /articles?fields=items(id,title),total ← JSON:API 风格(带资源前缀)
GET /articles?fields=id,title,author&expand=author ← 简化式:选字段 + 展关联fields:控制返回的字段列表,默认返回"安全全集";expand:把外键展开为内嵌对象(author={…}),未展开时只给{ "authorId": 7, "links": { "author": "/users/7" } };- 限制
expand深度与数量(如最多 2 个关联),防止被构造出 N+1 放大器。
6. 边界防御(必做清单)
- [ ]
pageSize/limit有服务端上限(如 100),超出取上限或 400,禁止无限大 - [ ] 默认值明确:
page=1&pageSize=20 - [ ] 空列表返回
items: []+ 200,不是 404(集合资源存在,只是为空) - [ ] 游标必须签名/加密或存 Redis——防篡改防深翻(cursor 是"不透明"的)
- [ ] 监控慢查询参数(如 offset 特别大的请求)并告警
- [ ]
total计算昂贵时可省略或返回"total": null+"totalRelation": "gte"(近似值语义,Elasticsearchtrack_total_hits思路)
7. 本章小结
- 外壳统一:
items + 分页字段 + links;分页语义与Link头(RFC 8288)是同一件事的两种表达。 - 偏移分页赢在跳页,游标分页赢在稳定与性能——按场景选,别混用两套参数。
- 过滤排序参数无官方标准,命名可以自选,纪律必须统一:白名单、拒绝未知参数、稳定默认排序、上限钳制。
8. 下一步
单个资源的响应长什么样、字段怎么命名 → 响应结构与命名