分页与过滤 #
概述 #
当 API 返回大量数据时,分页和过滤是必不可少的。它们可以减少网络传输、提高响应速度、改善用户体验。
text
┌─────────────────────────────────────────────────────────────┐
│ 分页与过滤的重要性 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 不分页的问题: │
│ ❌ 返回数据量过大 │
│ ❌ 响应时间长 │
│ ❌ 占用大量带宽 │
│ ❌ 客户端处理困难 │
│ ❌ 数据库压力大 │
│ │
│ 分页与过滤的好处: │
│ ✅ 减少数据传输量 │
│ ✅ 提高响应速度 │
│ ✅ 降低服务器负载 │
│ ✅ 改善用户体验 │
│ ✅ 支持大数据集 │
│ │
└─────────────────────────────────────────────────────────────┘
分页方式 #
页码分页(Page-based Pagination) #
text
┌─────────────────────────────────────────────────────────────┐
│ 页码分页 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 参数: │
│ - page:页码(从 1 开始) │
│ - limit/perPage:每页数量 │
│ │
│ 请求示例: │
│ GET /users?page=1&limit=20 │
│ GET /users?page=2&limit=20 │
│ │
│ 响应示例: │
│ { │
│ "data": [...], │
│ "pagination": { │
│ "page": 1, │
│ "limit": 20, │
│ "total": 100, │
│ "totalPages": 5 │
│ } │
│ } │
│ │
│ 优点: │
│ ✅ 简单直观 │
│ ✅ 支持跳页 │
│ ✅ 可以显示总页数 │
│ │
│ 缺点: │
│ ❌ 数据变更时可能重复或遗漏 │
│ ❌ 大偏移量性能差 │
│ ❌ 需要统计总数 │
│ │
│ SQL 实现: │
│ SELECT * FROM users │
│ LIMIT 20 OFFSET 0; -- 第 1 页 │
│ LIMIT 20 OFFSET 20; -- 第 2 页 │
│ │
└─────────────────────────────────────────────────────────────┘
偏移分页(Offset-based Pagination) #
text
┌─────────────────────────────────────────────────────────────┐
│ 偏移分页 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 参数: │
│ - offset:偏移量(从 0 开始) │
│ - limit:返回数量 │
│ │
│ 请求示例: │
│ GET /users?offset=0&limit=20 │
│ GET /users?offset=20&limit=20 │
│ GET /users?offset=40&limit=20 │
│ │
│ 响应示例: │
│ { │
│ "data": [...], │
│ "pagination": { │
│ "offset": 0, │
│ "limit": 20, │
│ "total": 100 │
│ } │
│ } │
│ │
│ 优点: │
│ ✅ 灵活性高 │
│ ✅ 适合无限滚动 │
│ │
│ 缺点: │
│ ❌ 大偏移量性能差 │
│ ❌ 数据变更时可能重复或遗漏 │
│ │
│ SQL 实现: │
│ SELECT * FROM users │
│ LIMIT 20 OFFSET 0; │
│ LIMIT 20 OFFSET 20; │
│ │
└─────────────────────────────────────────────────────────────┘
游标分页(Cursor-based Pagination) #
text
┌─────────────────────────────────────────────────────────────┐
│ 游标分页 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 参数: │
│ - cursor:游标(上一页最后一条记录的标识) │
│ - limit:返回数量 │
│ │
│ 请求示例: │
│ GET /users?limit=20 首页 │
│ GET /users?cursor=eyJpZCI6MjB9&limit=20 下一页 │
│ │
│ 响应示例: │
│ { │
│ "data": [...], │
│ "pagination": { │
│ "nextCursor": "eyJpZCI6NDB9", │
│ "previousCursor": "eyJpZCI6MH0=", │
│ "hasMore": true │
│ } │
│ } │
│ │
│ 优点: │
│ ✅ 性能稳定,不受数据量影响 │
│ ✅ 数据一致性,不会重复或遗漏 │
│ ✅ 适合实时数据 │
│ │
│ 缺点: │
│ ❌ 不支持跳页 │
│ ❌ 实现复杂 │
│ ❌ 不能显示总页数 │
│ │
│ SQL 实现: │
│ SELECT * FROM users │
│ WHERE id > 20 -- cursor 解析出的值 │
│ ORDER BY id ASC │
│ LIMIT 20; │
│ │
│ 游标编码: │
│ // Base64 编码 │
│ const cursor = Buffer.from(JSON.stringify({ id: 20 })) │
│ .toString('base64'); │
│ │
│ // 解码 │
│ const decoded = JSON.parse( │
│ Buffer.from(cursor, 'base64').toString() │
│ ); │
│ │
└─────────────────────────────────────────────────────────────┘
分页方式对比 #
text
┌─────────────────────────────────────────────────────────────┐
│ 分页方式对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 特性 页码分页 偏移分页 游标分页 │
│ ───────────────────────────────────────────────────────── │
│ 简单性 ⭐⭐⭐ ⭐⭐⭐ ⭐ │
│ 性能 ⭐ ⭐ ⭐⭐⭐ │
│ 一致性 ⭐ ⭐ ⭐⭐⭐ │
│ 跳页支持 ⭐⭐⭐ ⭐⭐ ❌ │
│ 总页数 ⭐⭐⭐ ⭐⭐ ❌ │
│ 无限滚动 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐ │
│ │
│ 选择建议: │
│ - 管理后台、需要跳页:页码分页 │
│ - 移动端、无限滚动:游标分页 │
│ - 简单场景:偏移分页 │
│ │
└─────────────────────────────────────────────────────────────┘
过滤(Filtering) #
过滤参数设计 #
text
┌─────────────────────────────────────────────────────────────┐
│ 过滤参数设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 精确匹配: │
│ GET /users?status=active │
│ GET /products?category=electronics │
│ │
│ 多值匹配: │
│ GET /users?status=active,pending │
│ GET /products?id=1,2,3 │
│ │
│ 范围过滤: │
│ GET /products?priceMin=100&priceMax=1000 │
│ GET /products?price_gte=100&price_lte=1000 │
│ GET /orders?createdAfter=2025-01-01 │
│ GET /orders?createdBefore=2025-12-31 │
│ │
│ 模糊匹配: │
│ GET /users?name_like=zhang │
│ GET /products?description_like=iPhone │
│ │
│ 存在性检查: │
│ GET /users?email_exists=true │
│ GET /products?discount_exists=false │
│ │
└─────────────────────────────────────────────────────────────┘
过滤操作符 #
text
┌─────────────────────────────────────────────────────────────┐
│ 过滤操作符 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 操作符 说明 示例 │
│ ───────────────────────────────────────────────────────── │
│ _eq 等于 ?status_eq=active │
│ _ne 不等于 ?status_ne=deleted │
│ _gt 大于 ?price_gt=100 │
│ _gte 大于等于 ?price_gte=100 │
│ _lt 小于 ?price_lt=1000 │
│ _lte 小于等于 ?price_lte=1000 │
│ _in 包含于 ?id_in=1,2,3 │
│ _nin 不包含于 ?status_nin=deleted,banned │
│ _like 模糊匹配 ?name_like=zhang │
│ _null 为空 ?deletedAt_null=true │
│ _nnull 不为空 ?email_nnull=true │
│ │
│ 示例: │
│ GET /products?price_gte=100&price_lte=1000 │
│ GET /users?status_in=active,pending │
│ GET /orders?createdAt_gte=2025-01-01 │
│ │
└─────────────────────────────────────────────────────────────┘
排序(Sorting) #
排序参数设计 #
text
┌─────────────────────────────────────────────────────────────┐
│ 排序参数设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 单字段排序: │
│ GET /users?sort=createdAt 升序 │
│ GET /users?sort=-createdAt 降序(- 前缀) │
│ GET /users?sort=name&order=asc 升序 │
│ GET /users?sort=name&order=desc 降序 │
│ │
│ 多字段排序: │
│ GET /users?sort=status,-createdAt │
│ 先按状态升序,再按创建时间降序 │
│ │
│ 默认排序: │
│ GET /users 默认按 createdAt 降序 │
│ │
│ 响应示例: │
│ { │
│ "data": [...], │
│ "sort": { │
│ "field": "createdAt", │
│ "order": "desc" │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────┘
排序实现 #
text
┌─────────────────────────────────────────────────────────────┐
│ 排序实现 │
├─────────────────────────────────────────────────────────────┤
│ │
│ // 解析排序参数 │
│ function parseSort(sortParam) { │
│ if (!sortParam) return [['createdAt', 'DESC']]; │
│ │
│ return sortParam.split(',').map(field => { │
│ if (field.startsWith('-')) { │
│ return [field.slice(1), 'DESC']; │
│ } │
│ return [field, 'ASC']; │
│ }); │
│ } │
│ │
│ // 示例 │
│ parseSort('name,-createdAt'); │
│ // [['name', 'ASC'], ['createdAt', 'DESC']] │
│ │
│ // SQL 构建 │
│ function buildOrderBy(sortFields) { │
│ return sortFields │
│ .map(([field, order]) => `${field} ${order}`) │
│ .join(', '); │
│ } │
│ │
│ // 安全验证:只允许特定字段排序 │
│ const allowedSortFields = ['id', 'name', 'createdAt']; │
│ │
│ function validateSort(sortParam) { │
│ const fields = parseSort(sortParam); │
│ return fields.filter(([field]) => │
│ allowedSortFields.includes(field) │
│ ); │
│ } │
│ │
└─────────────────────────────────────────────────────────────┘
字段选择(Field Selection) #
字段选择设计 #
text
┌─────────────────────────────────────────────────────────────┐
│ 字段选择设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 指定返回字段: │
│ GET /users?fields=id,name,email │
│ GET /products?fields=id,name,price │
│ │
│ 排除字段: │
│ GET /users?exclude=password,createdAt │
│ │
│ 嵌套字段: │
│ GET /orders?fields=id,total,user.id,user.name │
│ │
│ 响应示例: │
│ GET /users?fields=id,name │
│ { │
│ "data": [ │
│ { "id": 1, "name": "张三" }, │
│ { "id": 2, "name": "李四" } │
│ ] │
│ } │
│ │
│ 优点: │
│ ✅ 减少数据传输量 │
│ ✅ 提高响应速度 │
│ ✅ 保护敏感字段 │
│ │
└─────────────────────────────────────────────────────────────┘
搜索(Search) #
搜索设计 #
text
┌─────────────────────────────────────────────────────────────┐
│ 搜索设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 简单搜索: │
│ GET /users?q=zhang │
│ GET /products?search=iPhone │
│ │
│ 指定搜索字段: │
│ GET /users?q=zhang&searchFields=name,email │
│ │
│ 高级搜索: │
│ POST /search │
│ { │
│ "query": "iPhone", │
│ "filters": { │
│ "category": "electronics", │
│ "priceRange": { "min": 100, "max": 1000 } │
│ }, │
│ "sort": { "field": "price", "order": "asc" }, │
│ "fields": ["id", "name", "price"] │
│ } │
│ │
│ 响应示例: │
│ { │
│ "data": [...], │
│ "query": "iPhone", │
│ "total": 50, │
│ "took": 15 │
│ } │
│ │
└─────────────────────────────────────────────────────────────┘
完整查询示例 #
text
┌─────────────────────────────────────────────────────────────┐
│ 完整查询示例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 请求: │
│ GET /products? │
│ page=1& │
│ limit=20& │
│ category=electronics& │
│ price_gte=100& │
│ price_lte=1000& │
│ status=active& │
│ sort=-price& │
│ fields=id,name,price,category& │
│ q=iPhone │
│ │
│ 响应: │
│ { │
│ "data": [ │
│ { │
│ "id": 123, │
│ "name": "iPhone 15 Pro", │
│ "price": 999, │
│ "category": "electronics" │
│ } │
│ ], │
│ "pagination": { │
│ "page": 1, │
│ "limit": 20, │
│ "total": 50, │
│ "totalPages": 3 │
│ }, │
│ "filters": { │
│ "category": "electronics", │
│ "price": { "gte": 100, "lte": 1000 }, │
│ "status": "active" │
│ }, │
│ "sort": { │
│ "field": "price", │
│ "order": "desc" │
│ }, │
│ "query": "iPhone" │
│ } │
│ │
└─────────────────────────────────────────────────────────────┘
分页与过滤检查清单 #
text
□ 分页
□ 选择合适的分页方式
□ 设置默认分页参数
□ 限制最大返回数量
□ 返回分页元数据
□ 过滤
□ 支持常用过滤条件
□ 参数命名规范
□ 验证过滤参数
□ 优化查询性能
□ 排序
□ 支持多字段排序
□ 提供默认排序
□ 验证排序字段
□ 防止 SQL 注入
□ 字段选择
□ 支持指定返回字段
□ 保护敏感字段
□ 提供默认字段
□ 搜索
□ 支持全文搜索
□ 支持字段搜索
□ 返回搜索元数据
下一步 #
现在你已经了解了分页与过滤,接下来学习 错误处理,深入了解如何设计良好的错误响应!
最后更新:2026-03-29