總記錄數: $totalRows
每頁行數: $listRows
總頁數: $totalPages = ceil($totalRows / $listRows)
當前頁碼: $nowPage (1-based)
起始行號 (0-based): $firstRow = ($nowPage - 1) * $listRows
SQL LIMIT 子句: LIMIT $firstRow, $listRows
'',
'prev' => '« ',
'next' => '» ',
'first' => '«« ',
'last' => '»» ',
'page' => '%d ',
'current' => '%d ',
'disabled' => '%s ',
'ellipsis' => '… ',
'jump' => '',
];
public function __construct(int $totalRows = 0, int $listRows = 15) {
$this->totalRows = max(0, $totalRows);
$this->listRows = max(1, $listRows);
$this->calculate();
}
public function setTotalRows(int $totalRows): self {
$this->totalRows = max(0, $totalRows);
$this->calculate();
return $this;
}
public function setNowPage(int $page): self {
$this->nowPage = max(1, min($page, $this->totalPages ?: 1));
$this->firstRow = ($this->nowPage - 1) * $this->listRows;
return $this;
}
protected function calculate(): void {
$this->totalPages = (int)ceil($this->totalRows / $this->listRows);
if ($this->totalPages === 0) $this->totalPages = 1;
$this->nowPage = max(1, min($this->nowPage, $this->totalPages));
$this->firstRow = ($this->nowPage - 1) * $this->listRows;
}
public function getLimit(): string {
return "LIMIT {$this->firstRow}, {$this->listRows}";
}
public function getOffset(): int {
return $this->firstRow;
}
public function getLength(): int {
return $this->listRows;
}
// URL 構建策略
protected function buildUrl(int $page): string {
if ($this->urlPattern) {
return str_replace('{page}', (string)$page, $this->urlPattern);
}
$params = $this->queryParams;
$params['page'] = $page;
if ($this->parameter) {
parse_str($this->parameter, $extra);
$params = array_merge($params, $extra);
}
return '?' . http_build_query($params);
}
public function render(): string {
if ($this->totalRows === 0) return '';
$html = $this->config['header'];
$halfRoll = (int)floor($this->rollPage / 2);
$startPage = max(1, $this->nowPage - $halfRoll);
$endPage = min($this->totalPages, $startPage + $this->rollPage - 1);
if ($endPage - $startPage + 1 < $this->rollPage) {
$startPage = max(1, $endPage - $this->rollPage + 1);
}
// 首頁
if ($this->nowPage > 1) {
$html .= sprintf($this->config['first'], $this->buildUrl(1));
$html .= sprintf($this->config['prev'], $this->buildUrl($this->nowPage - 1));
} else {
$html .= sprintf($this->config['disabled'], '««');
$html .= sprintf($this->config['disabled'], '«');
}
// 省略號前
if ($startPage > 1) {
$html .= sprintf($this->config['page'], $this->buildUrl(1), 1);
if ($startPage > 2) $html .= $this->config['ellipsis'];
}
// 頁碼區
for ($i = $startPage; $i <= $endPage; $i++) {
if ($i === $this->nowPage) {
$html .= sprintf($this->config['current'], $i);
} else {
$html .= sprintf($this->config['page'], $this->buildUrl($i), $i);
}
}
// 省略號後
if ($endPage < $this->totalPages) {
if ($endPage < $this->totalPages - 1) $html .= $this->config['ellipsis'];
$html .= sprintf($this->config['page'], $this->buildUrl($this->totalPages), $this->totalPages);
}
// 尾頁
if ($this->nowPage < $this->totalPages) {
$html .= sprintf($this->config['next'], $this->buildUrl($this->nowPage + 1));
$html .= sprintf($this->config['last'], $this->buildUrl($this->totalPages));
} else {
$html .= sprintf($this->config['disabled'], '»');
$html .= sprintf($this->config['disabled'], '»»');
}
// 跳轉
if ($this->showPageJump && $this->totalPages > 1) {
$html .= sprintf(
$this->config['jump'],
$this->buildUrl(1), // form action 指向首頁或當前頁
'page', // 隱藏字段名 (可自定義)
$this->nowPage,
$this->totalPages,
$this->nowPage
);
}
$html .= $this->config['footer'];
return $html;
}
// 靜態工廠方法
public static function fromQuery(int $totalRows, int $listRows = 15, string $pageParam = 'page'): self {
$page = max(1, (int)($_GET[$pageParam] ?? 1));
$p = new self($totalRows, $listRows);
$p->setNowPage($page);
return $p;
}
}
query("SELECT COUNT(*) FROM articles WHERE status=1")->fetchColumn();
$paginator = Paginator::fromQuery($total, 20);
$articles = $db->query("SELECT * FROM articles WHERE status=1 ORDER BY id DESC " . $paginator->getLimit())->fetchAll();
// 視圖
echo $paginator->render();
// 自定義 URL 模式 (RESTful)
$paginator->urlPattern = '/articles/page/{page}';
$paginator->queryParams = ['category' => 'tech'];
echo $paginator->render();
// AJAX 分頁 (返回 JSON)
if ($_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
header('Content-Type: application/json');
echo json_encode([
'html' => $articleListHtml,
'pagination' => $paginator->render(),
'current_page' => $paginator->nowPage,
'total_pages' => $paginator->totalPages,
]);
exit;
}
| 策略 | 實現 | 效果 |
|---|---|---|
| 語義化 HTML | <nav aria-label="Pagination"><ul class="pagination"> | 蜘蛛識別導航結構 |
| rel="prev/next" | 首頁/上一頁/下一頁/尾頁鏈接添加 rel="prev" / rel="next" | Google 理解分頁系列,合併索引信號 |
| Canonical 標準化 | 首頁 <link rel="canonical" href="/list">,分頁頁 href="/list/page/2" | 避免重複內容懲罰 |
| 避免 JS 渲染分頁 | 服務端直出 HTML,或 SSR/預渲染 | 確保蜘蛛可抓取所有頁碼鏈接 |
| 載入更多/無限滾動 | 配合 IntersectionObserver,但保留傳統分頁作為降級 | 體驗與 SEO 平衡 |
-- 偏移分頁 (深分頁性能劣化)
SELECT * FROM t ORDER BY id LIMIT 1000000, 20; -- 掃描 100萬行丟棄
-- 游標分頁 (Keyset Pagination, 適合無限滾動/API)
SELECT * FROM t WHERE id > :last_seen_id ORDER BY id LIMIT 20; -- 索引命中,極快
-- 需唯一、有序、不變的列 (主鍵、創建時間+ID)
分頁類是 Web 應用中最高頻復用組件之一。優秀的分頁類應具備:核心算法正確、URL 構建靈活、模板可定制、SEO 語義化、大數據量優化策略、零依賴易集成。本文重構版採用配置驅動模板、策略模式構建 URL、工廠方法簡化實例化,可直接投入生產或作為框架分頁組件的參考實現。記住:分頁不僅是切片數據,更是用戶導航、蜘蛛爬取、系統性能的關鍵交匯點。