← 返回目录

2. 分页逻辑与边界场景

分页就是数据太多分几页显示,每页显示固定条数。

核心三步:
算总页数——Math.ceil(总数 / 每页条数)
切片数据——slice((当前页-1)*每页, 当前页*每页)
渲染分页按钮——上一页/页码/下一页

坑:当前页不能小于 1 不能大于总页数、切片从 0 开始、页码按钮要高亮当前页

2.1 先跑起来:35 条数据的分页演示

ID姓名年龄操作

试试:① 删掉当前页最后一条数据 → 自动回退上一页;② 搜索后页码自动回到第 1 页;③ 首页、中间页、尾页的省略号位置。

2.2 分页核心三要素 + 截取公式(必背)

// 当前页码(从第 1 页开始,不是 0)→ 你现在翻到第几页
let currentPage = 1;

// 每页几条 → 一页固定摆几条
const pageSize = 5;

// 总数据条数 → 一共有多少条
let total = allData.length;

// 总页数,向上取整 → 一共能分成几页
let totalPages = Math.ceil(total / pageSize);

// 截取当前页数据:含头不含尾(slice 的特性)→ slice 包头不包尾
// 本页第一条的下标 → 从第几条开始
const startIndex = (currentPage - 1) * pageSize;

// 本页最后一条的下一个下标 → 取到第几条为止
const endIndex = currentPage * pageSize;

// 把这一页的几条切出来
const currentData = allData.slice(startIndex, endIndex);

先理解下标怎么算,分四步:
1. 每页 5 条时各页取哪些:第 1 页取 0~5,第 2 页取 5~10,第 3 页取 10~15。
2. 本页第一条下标怎么算: = (currentPage - 1) × pageSize
3. 为什么总页数用 Math.ceil(向上取整):25 条 ÷ 每页 10 条 = 2.5 页,你不能说"只有 2.5 页",最后半页那 5 条也得有个地方放,所以向上取成 3 页。
4. slice 左含右不含:所以 endIndex 直接写 currentPage × pageSize 就行,千万别写成 (currentPage+1)×pageSize,那样每页就多切一条了。

2.3 渲染页码(带省略号,固定显示 7 个位置)

function renderPagination() {
  // 先准备空字符串攒 HTML
  let html = "";

  // 把总页数、当前页取出来省事
  const total = totalPages,
    current = currentPage;

  // 最多显示 7 个位置
  const maxShow = 7;

  // 先把要显示哪些装进数组
  let pages = [];

  if (total <= maxShow) {
    // 页数少就全列出来
    for (let i = 1; i <= total; i++) pages.push(i);
  } else {
    // 数字画按钮,当前页高亮
    // 首页永远放 1
    pages.push(1);

    // 当前页左右各留 2 页
    let midStart = current - 2,
      midEnd = current + 2;

    if (midStart <= 2) {
      // 当前页太靠左
      // 中间段从 2 开始
      midStart = 2;
      // 右边别超尾页前一个
      midEnd = Math.min(total - 1, midStart + 4);
    }

    if (midEnd >= total - 1) {
      // 当前页太靠右
      // 中间段收到尾页前一个
      midEnd = total - 1;
      // 左边别小于 2
      midStart = Math.max(2, midEnd - 4);
    }

    // 左边空出一截就打 ...
    if (midStart > 2) pages.push("...");

    // 中间连续页码
    for (let i = midStart; i <= midEnd; i++) pages.push(i);

    // 右边空出一截就打 ...
    if (midEnd < total - 1) pages.push("...");

    // 尾页永远放总页数
    pages.push(total);

    // 去重,避免两个 1
    pages = Array.from(new Set(pages));
  }

  // 不是首页才显示上一页
  html += current > 1 ? '<button data-page="' + (current - 1) + '">上一页</button>' : '';

  // 一个一个拼页码按钮
  pages.forEach(function(p) {
    if (p === "...") {
      // 省略号画个不可点的点
      html += '...';
    } else {
      // 当前页加高亮
      let active = (p === current ? "active" : "");
      html += '';
    }
  });

  // 不是尾页才显示下一页
  html += current < total ? '<button data-page="' + (current + 1) + '">下一页</button>' : '';

  // 整排塞进页面
  $("#pagination").html(html);
}

这一段分四步:
1. 先把要显示哪些装进数组:可能是数字,也可能是 "...",再统一渲染,比边判断边拼字符串清楚得多。
2. 首尾永远放 1 和 total:中间放当前页附近的几页。
3. 放不下的用 "..." 占位:页太多时中间省略。
4. 最后用 new Set 去重:因为当前页靠近第 1 页时,中间段会和"首页"撞成同一个数,不去重就会出现两个"1"。

2.4 边界 1:删空当前页 → 页码自动回退

// 现在还剩几条
total = allUsers.length;

// 还能分成几页
totalPages = Math.ceil(total / pageSize);

const currentPageData = allUsers.slice((currentPage - 1) * pageSize, currentPage * pageSize);

// 这页空了,且不是第 1 页
if (currentPageData.length === 0 && currentPage > 1) {
  // ★ 关键:页码减一
  currentPage--;
}

// 重画表格数据
renderPageData(currentPage);

// 重画页码按钮
renderPagination();

举个例子,分四步理解:
1. 问题场景:你在第 3 页,把这页 5 条全删了,第 3 页就空了,表格一片空白但页码还亮着"3",用户以为数据丢了。
2. 正确做法:判断"本页数据为空"且"不是第 1 页"两个条件同时成立,就 currentPage--
3. 为什么要加"不是第 1 页":因为如果本来就在第 1 页,再减一就变成第 0 页了,那是非法页码。
4. 回退后必须同时重画"表格"和"页码"两处:漏一处就会错位。

2.5 边界 2:搜索/筛选后,页码要回到第 1 页

$("#searchBtn").click(function () {
  // 取关键词,去掉两头空格
  const kw = $("#kw").val().trim();

  // ★ 必须基于原始备份 originalUsers 筛选,不能在结果上接着筛
  if (kw) {
    allUsers = originalUsers.filter(function (u) {
      return u.name.includes(kw);
    });
  } else {
    allUsers = originalUsers.slice();
  }

  // 筛完还剩几条
  total = allUsers.length;

  // 重算总页数
  totalPages = Math.ceil(total / pageSize);

  // 搜完回第 1 页
  currentPage = 1;

  // 重画表格
  renderPageData(currentPage);

  // 重画页码
  renderPagination();
});

$("#resetBtn").click(function () {
  // ① 清空输入框
  $("#kw").val("");

  // ② 数据恢复原样
  allUsers = originalUsers.slice();

  // ③ 页码回 1
  currentPage = 1;

  renderPageData(currentPage);
  renderPagination();
});

两个新手必踩的坑:
筛选必须基于 originalUsers 备份。如果你直接在 allUsers 上反复 filter,第一次筛剩 20 条,第二次在这 20 条上再筛,结果会越筛越少,像滚雪球一样不可控。永远是"从原始数据重新算"。
搜完必须 currentPage = 1。设想你之前翻到了第 5 页,然后输入一个关键词,结果只剩 2 条(只有 1 页),这时 currentPage 还是 5,但 totalPages 已经是 1 了,你就停在一个不存在的页码上,表格空白。
重置按钮就是"清空输入框 + 恢复原始数据 + 页码回 1"三件套。

2.6 前端分页 vs 后端分页(什么时候用哪种)

前端分页(客户端) 后端分页(服务端)
// 一次性把所有数据拿回浏览器 → 数据全在本地
// 翻页时前端用 slice 自己切 → 不找服务器,自己切
// 翻页不发网络请求,秒切,体验好 → 切页飞快
// 适合 < 1000 条的小数据 → 数据少时这么做
// 每次翻页都请求一次接口 → 翻一页就找一次服务器
// 跟后端要数据
$.ajax({
  // 数据接口地址
  url: "/api/users",

  // 告诉后端:我要第几页、每页几条
  data: { page, pageSize },

  // 后端把这页数据送回来(普通 function)
  success: function (res) {
    // 后端只返回当前这一页 → 直接画这页
    renderPageData(res.data.list);

    // 后端还告诉了总条数,据此算总页数
    totalPages = Math.ceil(res.data.total / pageSize);
  }
});

// 适合几万、几十万条的海量数据 → 数据多时这么做,别一次全拉下来

记一句话:数据少用前端切(快),数据多用后端查(轻)。分两种情况:
1. 前端分页(数据少):本页演示就是,把 35 条数据直接写死在代码里,翻页时用 slice 自己切,不找服务器。
2. 后端分页(数据多):真实项目动辄几万条,不可能一次全拉下来。前端把"第几页、每页几条"发给接口,后端只返回那一页的数据。

2.7 实战:用在哪 / 常见坑 / 怎么解决

① 可能在什么地方用:后台管理系统的用户列表、订单列表、商品列表;电商商品搜索结果翻页;论坛帖子列表——凡是"数据超过一屏"的表格,都要分页。

② 常见的问题:删掉最后一条后表格空白(忘了 currentPage-- 回退);搜索后停在不存在的页码(忘了 currentPage = 1);页码出现两个"1"或"..."重复(忘了用 Set 去重);越筛越少(在已筛结果上接着筛);slice 切多了或切少了(startIndex/endIndex 公式写错)。

③ 解决思路:在控制台把 totaltotalPagescurrentPage 打出来,对照着看:删完数据后这三个数变了没?搜完 currentPage 是不是回到 1 了?手动算一下第 3 页的 startIndex 应该是 10,slice 出来的数组长度对不对。边界场景(首页、尾页、删空、搜空)挨个点一遍,bug 基本都藏在这几个角落。

一句话:分页 = 三要素(page / pageSize / total)+ 截取公式(slice 左含右不含);难点全在边界——删空了要回退,筛完了要回第 1 页,页多了要省略号,首尾永远放 1 和 total。

本页重点

API作用参数怎么传返回/结果代码示例
slice从数组里切出当前页那几条(起,止) 左含右不含新数组let data = allUsers.slice(0, 5);
Math.ceil算出一共几页(总数/每页条数)向上取整的页数let totalPages = Math.ceil(total / 5);
splice删掉表格里那一条(下标, 1)删完的数组allUsers.splice(2, 1);
findIndex按 id 找到是第几项(条件函数)下标,找不到 -1let idx = allUsers.findIndex(function(u) { return u.id===id; });
parseInt把页码字符串转成数字("5", 10)数字 5let page = parseInt(btn.dataset.page, 10);