返回博客

分页省略号坏味道:「1 … 3」比「1 2 3」更难看

给某个后台管理系统实现分页导航时,页码序列渲染出了 1 … 3 4 5 … 20。第一个省略号里藏的是什么?一翻源码——就是数字 2。省略号占的宽度和一个数字完全相同,却让用户无法直接点击,还得心算「原来这里跳过了一页」。纯粹的信息损耗。

现象

分页组件在接近边界时出现 1 … 318 … 20 这类序列。省略号()展开之后发现里面只藏了一个页码。视觉上毫无增益,交互上反而退步。

根因

朴素实现会对「当前页窗口」与「首尾边界页」之间任意 gap 一律插省略号:

// 朴素写法——gap 存在就插省略号,无论 gap 大小
if (p - prev > 1) out.push("ellipsis");
out.push(p);

page=3, total=10 时,首页 1 与窗口左边界 2 之间 diff 为 2,条件成立,插入省略号——但 diff 为 2 意味着中间只有 1 个页码prev + 1 = 2)。省略号的宽度等于被折叠数字的宽度,这笔交易稳亏。

坑:gap === 1 时插省略号是负优化

省略号的存在价值是「折叠多个不值得展示的页码」。当被折叠的页码只有 1 个时,省略号自身占的空间与那个页码相同,却剥夺了用户直接点击的能力,同时增加认知摩擦。条件应当是 gap > 1,而非 gap >= 1

正解

Set 收集必须显示的页码(首尾 + 当前页窗口),排序后遍历相邻对,按 diff 大小分三种情况处理:

  • diff === 1:连续,不插任何东西
  • diff === 2:gap 恰好 1 个页码,直接填入 prev + 1
  • diff > 2:gap 多于 1 个,插 "ellipsis"
export type PaginationItem = number | "ellipsis";
 
const clamp = (n: number, lo: number, hi: number) =>
  Math.min(Math.max(n, lo), hi);
 
export function getPaginationRange({
  page,
  total,
  siblingCount = 1,
}: {
  page: number;
  total: number;
  siblingCount?: number;
}): PaginationItem[] {
  if (!Number.isFinite(total) || total < 1) return [];
  const cur = clamp(Math.trunc(page), 1, total);
  const sib = Math.max(0, Math.trunc(siblingCount));
 
  const shown = new Set<number>([1, total]);
  for (let p = cur - sib; p <= cur + sib; p++) {
    if (p >= 1 && p <= total) shown.add(p);
  }
 
  const sorted = [...shown].sort((a, b) => a - b);
  const out: PaginationItem[] = [];
  let prev = 0;
  for (const p of sorted) {
    if (prev) {
      if (p - prev === 2) out.push(prev + 1);   // 单 gap → 填数字
      else if (p - prev > 2) out.push("ellipsis"); // 多 gap → 省略号
    }
    out.push(p);
    prev = p;
  }
  return out;
}
正解:diff === 2 填数字,diff > 2 才给省略号

关键边界用例(必须全绿才算做对):

| 输入 | 期望输出 | |------|---------| | page=3, total=5 | [1,2,3,4,5](全展开,无省略号) | | page=4, total=7 | [1,2,3,4,5,6,7](两侧 gap 各 1,全填数字) | | page=4, total=8 | [1,2,3,4,5,"ellipsis",8](左侧 gap=1 填数字,右侧 gap>1 省略号) | | page=5, total=10 | [1,"ellipsis",4,5,6,"ellipsis",10] |

不变式:每个 "ellipsis" 两侧的实数之差必须 > 2

实现建议

getPaginationRange 抽为纯函数独立模块pagination.range.ts),不依赖任何框架——逻辑完全可单元测试,且在 RSC 环境下安全引用。含 onClick 的渲染层独立为客户端组件,从纯函数拿到序列后只做展示。

省略号 DOM 建议写法(无障碍友好):

<span>
  <span aria-hidden="true">…</span>
  <span className="sr-only">更多页</span>
</span>

外层不加 aria-hidden,这样屏幕阅读器会朗读"更多页"而不是静默跳过。

一句话外卖

省略号只有在折叠多于 1 个页码时才有价值;gap 为 1 时直接填数字,严格优于省略号。