12 — 分页与搜索

3 阅读3分钟

12.2 offset + size 分页

参数约定

参数类型默认值说明
offsetnumber0从第几条开始(0 表示第一条)
sizenumber10每页多少条
name / keywordstring-搜索关键词

响应约定

{
  "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 分页性能注意事项

  1. offset 越大越慢OFFSET 100000 数据库还是要扫前面 10 万条。如果数据量大,考虑用游标分页。
  2. LIKE ‘%xxx%’ 走不了索引:前后都有通配符的模糊搜索无法利用普通索引。数据量大时需要用全文索引或搜索引擎。
  3. size 要有上限:防止有人传 size=999999 一次把所有数据拉出来。
  4. count 查询也慢:大表 COUNT(*) 本身就慢,可以考虑缓存总数或用估算。

管理后台项目数据量一般不大,这些优化基本用不上,但要有这个意识。


12.9 小结

  • 分页参数:offset + size,响应带回 count
  • 搜索:模糊匹配,多条件组合
  • 排序:sortField + sortOrder
  • 分页工具函数统一解析参数
  • size 要有上限,防止恶意请求
  • 数据量小时 Service 层做过滤就行,数据量大了用数据库分页