本文首发于:kapeter.com/post/34
文章列表:
这是Eggjs最佳实践系列的第三篇文章。
前期调研
分页接口是在大型系统中经常出现的接口形式。通过调研,目前主要存在以下三种分页方案:基于偏移、基于游标、基于ID列表。其中基于ID列表仅适用于数量较少(数百条)的应用场景,不做考虑。
基于偏移的分页方案
这是最传统的分页方案,目前多用于PC端,如PC端的谷歌搜索,原理主要通过数据库的Limit获取分页数据。
前端表现为:
- 通过页码进行分页;
- 通过点击上/下页按钮可实现页面切换;
- 通过点击页码可实现页面切换;
请求参数多为:
| 参数 | 含义 | 备注 | 
|---|---|---|
| page | 当前页数 | 必传 | 
| size / limit | 每页数据个数 | 可选,可以前端传参,也可以后端写死 | 
还有一种,是前端计算offset(偏移量)后,再传给后端。但考虑到数据完整性,建议把计算offset(偏移量)的工作放在后端。
该方案的优势:
- 有清晰的边界;
- 可直接跳转至指定页面。
该方案的劣势:
- 存在数据重复或者缺失问题(有技术手段可以优化);
- 数据量大后存在性能问题(有技术手段可以优化)。
基于游标的分页方案
该方案多出现在基于时间轴的社交接口上,如新浪微博。
前端多表现为:
- 通过滚动/上拉/点击等方式加载新一页
- 无页码
- 无上/下页按钮
- 不可跳转至指定页面
请求参数多为:
| 参数 | 含义 | 备注 | 
|---|---|---|
| cursor / since_id | 当前游标,一般为当前列表最后一位的ID | 必传 | 
| size / limit | 每页数据个数 | 可选,可以前端传参,也可以后端写死 | 
该方案的优势:
- 无感知刷新;
- 不存在数据重复或者缺失问题;
- 性能好于基于偏移的方案
该方案的劣势:
- 无法直接跳转到对应内容;
- 数据量大后,前端渲染压力大。
可以发现,两种分页方案各有利弊,需要根据业务需求进行取舍。我目前做的是PC端的内部平台,因此选择基于偏移的分页方案。
技术实现
假设我们现在需要设计一个获取项目列表的(/project/list)接口。
首先,我们来设计一下请求参数。
// 请求参数
{
  "page":1,
  "size": 10,
  "filter": {
    "status": true
    // ……
  }
}
复制代码我在page和size基础上,新增一个filter参数,可以让用户进一步筛选,也就是说将列表接口和搜索接口合在一起。
据此,我们可以写出这个参数的校验规则。
const listRule = {
  page: { type: 'integer', required: true, min: 1 },
  size: { type: 'integer', required: false, max: 100, default: 10 },
  filter: { type: 'object', required: false, default: {} },
};
复制代码并完成controller方法的代码编写。
/**
 * @description 路由方法-获取列表
 * @memberof ProjectController
 */
async index() {
  const { ctx } = this;
  try {
    // 参数校验
    ctx.validate(listRule);
    const { page, size, filter } = ctx.request.body;
    // 获取结果
    const result = await ctx.service.project.pagingList({
      page,
      size,
      filter,
    });
    // 接口返回
    ctx.response.success({ data: result });
  } catch (error) {
    ctx.response.handleCommonErr(error);
  }
}
复制代码controller部分比较简单,就是校验参数,调用service获取数据,然后返回结果。
重点来看service部分。
/**
 * @description 列表查询
 * @param {Object} query 查询条件
 * @return {Object} 结果
 * @memberof ProjectService
 */
async pagingList(query) {
  const { ctx, app } = this;
  const { Op } = app.Sequelize;
  const { size, page, filter } = query;
  // 计算分页偏移量
  const offset = ctx.helper.calcPagingOffset(page, size);
  // 处理条件约束
  let whereObj = {};
  if (ctx.helper.isObject(filter) && !ctx.helper.isEmptyObject(filter)) {
    for (const x in filter) {
      switch (x) {
        case 'alias':
          whereObj.alias = { [Op.like]: `%${filter.alias}%` };
          break;
        case 'status':
          whereObj.status = filter.status;
          break;
        case 'deleted_at':
          whereObj.deletedAt = { [Op.ne]: null };
          break;
        default:
          break;
      }
    }
  }
  // 请求数据库
  const result = await ctx.model.Project.findAndCountAll({
    attributes: this.attributes,
    where: whereObj,
    limit: size ? size : null,
    offset: offset ? offset : null,
    order: [
      ['created_time', 'desc'],
    ],
  });
  // 格式化数据并返回
  return ctx.helper.formatPagingData({ page, size, count: result.count, list: result.rows });
}
复制代码上文提到,需要把计算偏移量(offset)的工作放到后端,因此,我们第一步就是根据page和size计算偏移量。
/**
 * @description 计算偏移值
 * @param {Number} page 当前页数
 * @param {Number} size 每页数量
 * @return {Number} 数据库中的偏移值
 */
calcPagingOffset(page, size) {
  return (size && page) ? size * (page - 1) : 0;
},
复制代码然后去处理前端传入的筛选条件,把他放到where对象中。
接着去数据库中获取数据,根据数据库规范,不能通过select * from获取数据,因此建议,设置一个attributes返回需要获取的字段列表。
数据获取后,我们还要对整个格式进行处理,把分页信息输出给前端。
/**
 * @description 格式化分页数据
 * @param {*} { page, size, count, list } - page - 当前页数,size - 每页数量, count - 总数, list - 数据
 * @return {*} 格式化后的数据
 */
formatPagingData({ page, size, count, list }) {
  const totalPage = Math.ceil(count / size);
  return {
    currentPage: page,
    totalPage,
    size,
    hasPrev: page > 1 && page < totalPage,
    hasNext: page < totalPage,
    total: count,
    list: list || [],
  };
},
复制代码最终输出格式为:
{
    "code": "0",
    "message": "success",
    "result": {
        "currentPage": 1,
        "totalPage": 2,
        "size": 10,
        "hasPrev": false,
        "hasNext": false,
        "total": 20,
        "list": [
          // 列表数据
        ]
    },
    "sysTime": 1622626911874
}
复制代码至此,一个分页接口初步完成。
但刚才也提到了,基于偏移的分页方案存在许多问题,后续我将在实践中慢慢去优化。






















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
