12.2 offset + size 分页
参数约定
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| offset | number | 0 | 从第几条开始(0 表示第一条) |
| size | number | 10 | 每页多少条 |
| name / keyword | string | - | 搜索关键词 |
响应约定
{
"status": 0,
"data": [...],
"count": 100,
"offset": 0,
"size": 10
}
count:总条数offset:当前偏移size:每页大小
前端用这三个值计算总页数和当前页。
12.3 分页工具函数
src/utils/pagination.ts:
const DEFAULT_OFFSET = 0;
const DEFAULT_SIZE = 10;
const MAX_SIZE = 100;
// 解析 offset
export function parseOffset(value: unknown): number {
const num = Number(value);
return Number.isFinite(num) && num >= 0 ? Math.floor(num) : DEFAULT_OFFSET;
}
// 解析 size(有上限,防止有人传 99999 搞崩服务器)
export function parseSize(value: unknown): number {
const num = Number(value);
if (!Number.isFinite(num) || num <= 0) return DEFAULT_SIZE;
return Math.min(Math.floor(num), MAX_SIZE);
}
// 解析搜索文本
export function parseText(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
// 解析排序参数
export function parseSort(value: unknown): { field: string; order: 'asc' | 'desc' } | null {
if (!value || typeof value !== 'string') return null;
const [field, order] = value.split(',');
if (!field) return null;
const o = order === 'desc' ? 'desc' : 'asc';
return { field, order: o };
}
12.4 Service 层的分页
Service 层返回全部过滤后的数据,分页切割放在 Controller 层做(或者 Model 层做,看项目约定)。
方式一:Service 返回全部,Controller 分页
// Service
list(name = ''): Project[] {
return this.projects
.list()
.filter((p) => !name || p.name.toLowerCase().includes(name.toLowerCase()));
}
// Controller
list = (req: Request, res: Response) => {
const offset = parseOffset(req.query.offset);
const size = parseSize(req.query.size);
const name = parseText(req.query.name);
const all = this.service.list(name);
return sendList(res, all.slice(offset, offset + size), {
count: all.length,
offset,
size,
});
};
简单直观,适合数据量不大的情况。
方式二:Service 层分页,返回分页结果
// Service
list(params: { offset: number; size: number; name?: string }) {
const all = this.projects.list();
const filtered = all.filter(
(p) => !params.name || p.name.toLowerCase().includes(params.name!.toLowerCase()),
);
return {
list: filtered.slice(params.offset, params.offset + params.size),
count: filtered.length,
};
}
适合数据库场景,因为数据库分页查询本身就返回 count 和 list。
12.5 模糊搜索
按单个字段搜索
list(name = ''): Project[] {
const all = this.projects.list();
if (!name) return all;
const lower = name.toLowerCase();
return all.filter((p) => p.name.toLowerCase().includes(lower));
}
按多个字段搜索
list(keyword = ''): Project[] {
const all = this.projects.list();
if (!keyword) return all;
const lower = keyword.toLowerCase();
return all.filter(
(p) =>
p.name.toLowerCase().includes(lower) ||
p.description.toLowerCase().includes(lower),
);
}
多个搜索条件组合
list(params: { name?: string; status?: string }): Project[] {
let result = this.projects.list();
if (params.name) {
const lower = params.name.toLowerCase();
result = result.filter((p) => p.name.toLowerCase().includes(lower));
}
if (params.status) {
result = result.filter((p) => p.status === params.status);
}
return result;
}
12.6 排序
单字段排序
list(sortField?: string, sortOrder: 'asc' | 'desc' = 'asc'): Project[] {
const list = this.projects.list();
if (!sortField) return list;
return [...list].sort((a, b) => {
const aVal = a[sortField as keyof Project];
const bVal = b[sortField as keyof Project];
if (aVal === bVal) return 0;
const result = aVal > bVal ? 1 : -1;
return sortOrder === 'asc' ? result : -result;
});
}
请求示例
GET /api/v1/projects?sort=created_at,desc
12.7 数据库场景下的分页
换成真实数据库后,分页逻辑在 SQL 层面做:
-- MySQL
SELECT * FROM projects
WHERE name LIKE '%foo%'
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
-- 同时查总数
SELECT COUNT(*) FROM projects WHERE name LIKE '%foo%';
或者用数据库驱动/ORM 提供的分页方法。这时候分页逻辑应该放在 Model 层,Service 层传参。
12.8 分页性能注意事项
- offset 越大越慢:
OFFSET 100000数据库还是要扫前面 10 万条。如果数据量大,考虑用游标分页。 - LIKE ‘%xxx%’ 走不了索引:前后都有通配符的模糊搜索无法利用普通索引。数据量大时需要用全文索引或搜索引擎。
- size 要有上限:防止有人传
size=999999一次把所有数据拉出来。 - count 查询也慢:大表
COUNT(*)本身就慢,可以考虑缓存总数或用估算。
管理后台项目数据量一般不大,这些优化基本用不上,但要有这个意识。
12.9 小结
- 分页参数:offset + size,响应带回 count
- 搜索:模糊匹配,多条件组合
- 排序:sortField + sortOrder
- 分页工具函数统一解析参数
- size 要有上限,防止恶意请求
- 数据量小时 Service 层做过滤就行,数据量大了用数据库分页