← 返回目录

2. 分页逻辑与边界场景

把这页想成"图书馆排书架":35 本书一口气全摊在桌上没法看,于是规定每层摆 5 本(pageSize),总共有几层(totalPages)、你现在站在第几层(currentPage)。真正的难点不是"切页"这一下,而是那些容易被忽略的边界情况:把某层最后一本抽走了、在搜索框输入了关键词、页码多到一屏放不下要省略号。这页三个数 page / pageSize / total 背下来,分页就会一半。

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

ID姓名年龄操作

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

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

let currentPage = 1; // 当前页码(从第 1 页开始,不是 0)→ 你现在翻到第几页
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); // 把这一页的几条切出来

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

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

function renderPagination() {       // 重画底部那一排页码
  let html = "";                    // 先准备一个空字符串攒 HTML
  const total = totalPages,
    current = currentPage;          // 把总页数、当前页取出来省事
  const maxShow = 7; // 最多显示 7 个位置,多了就用省略号代替 → 一屏最多放 7 个格子
  let pages = [];                   // 先把"要显示哪些"装进这个数组

  if (total <= maxShow) {           // 页数很少(7 页以内)
    for (let i = 1; i <= total; i++) pages.push(i); // 页数少,干脆全显示 → 1、2、3...全列出来
  } else {                          // 页数太多,要省略号
    pages.push(1); // 1. 永远先放"首页" → 第一个永远是 1
    let midStart = current - 2,
      midEnd = current + 2; // 2. 当前页左右各留 2 页 → 当前页前后各显示 2 个
    if (midStart <= 2) {            // 当前页太靠左边了
      midStart = 2;                 // 中间段从 2 开始
      midEnd = Math.min(total - 1, midStart + 4);  // 右边别超过尾页前一个
    }
    if (midEnd >= total - 1) {      // 当前页太靠右边了
      midEnd = total - 1;           // 中间段收到尾页前一个
      midStart = Math.max(2, midEnd - 4);  // 左边别小于 2
    }
    if (midStart > 2) pages.push("..."); // 3. 左边和首页之间塞省略号 → 左边空出一截就打 ...
    for (let i = midStart; i <= midEnd; i++) pages.push(i); // 4. 中间连续页码 → 把这几页都放进去
    if (midEnd < total - 1) pages.push("..."); // 5. 右边和尾页之间塞省略号 → 右边空出一截就打 ...
    pages.push(total); // 6. 永远最后放"尾页" → 最后一个永远是总页数
    // 7. 去重:防止首页和中间段撞车,比如 current=1 时会重复
    pages = [...new Set(pages)];    // 用 Set 去重,避免出现两个 1
  }

  // 拼 HTML:当前页加 active 高亮,省略号用 span(不可点)
  html += current > 1 ? `<button data-page="${current - 1}">上一页</button>` : "";  // 不是首页才显示"上一页"
  pages.forEach(p => {              // 一个一个把页码拼成按钮
    html += p === "..." ? `<span class="dot">...</span>` :   // 是省略号就画个不可点的点
      `<button class="${p === current ? "active" : ""}" data-page="${p}">${p}</button>`; // 数字就画按钮,当前页加高亮
  });
  html += current < total ? `<button data-page="${current + 1}">下一页</button>` : "";  // 不是尾页才显示"下一页"
  document.getElementById("pagination").innerHTML = html; // 整排塞进页面
}

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

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

// 删除后:先更新 total 和 totalPages → 数据少了,总数和总页数要重算
total = allUsers.length;            // 现在还剩几条
totalPages = Math.ceil(total / pageSize);  // 还能分成几页

// 再检查:当前页是不是被删空了?而且不是第 1 页 → 就回退一页
const currentPageData = allUsers.slice((currentPage - 1) * pageSize, currentPage * pageSize); // 把这页再切出来看看
if (currentPageData.length === 0 &amp;&amp; currentPage > 1) {  // 这页一条都没剩,且不是第 1 页
  currentPage--; // ★ 关键:页码减一 → 退回到上一页
}
renderPageData(currentPage); // 重画表格数据 → 把上一页的数据画出来
renderPagination; // 重画页码按钮 → 页码高亮也跟着退回去

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

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

$("#searchBtn").click(function() {    // 点"搜索"按钮
 const kw = $("#kw").val.trim();     // 取输入框里的关键词,去掉两头空格
 // ★ 必须基于【原始备份】originalUsers 筛选,不能在已经筛过的结果上接着筛
 allUsers = kw                       // 有关键词就筛,没关键词就全部
  ? originalUsers.filter(u => u.name.includes(kw))  // 从原始数据里挑名字含关键词的
  : [...originalUsers];              // 没关键词:把原始数据整个复制一份
 total = allUsers.length;            // 筛完还剩几条
 totalPages = Math.ceil(total / pageSize);  // 重算总页数
 currentPage = 1;  // ★ 搜完一律回第 1 页 → 别停在原来那页
 renderPageData(currentPage);        // 重画表格
 renderPagination;                   // 重画页码
});

// 重置:三步缺一不可
$("#resetBtn").click(function() {    // 点"重置"按钮
 $("#kw").val("");         // ① 清空输入框
 allUsers = [...originalUsers];  // ② 数据恢复原样 → 从原始备份重新复制一份
 currentPage = 1;         // ③ 页码也回 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 },         // 告诉后端:我要第几页、每页几条
  success: res => {                // 后端把这页数据送回来
    renderPageData(res.data.list); // 后端只返回当前这一页 → 直接画这页
    totalPages = Math.ceil(res.data.total / pageSize); // 后端还告诉了总条数,据此算总页数
  }
});
// 适合几万、几十万条的海量数据 → 数据多时这么做,别一次全拉下来

记一句话:数据少用前端切(快),数据多用后端查(轻)。本页演示是前端分页,因为我们把 35 条数据直接写死在代码里了;真实项目动辄几万条,不可能一次全拉下来,就得后端分页——前端把"第几页、每页几条"发给接口,后端只返回那一页的数据。

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。