分頁是數據量大時的必備功能。本文提供從傳統 URL 分頁到現代 AJAX 無刷新加載的完整實現,涵蓋邏輯算法、資料庫查詢、前端渲染、用戶體驗優化。
<?php
// 輸入參數
$page = max(1, (int)($_GET['p'] ?? 1)); // 當前頁碼,最小 1
$pageSize = 10; // 每頁筆數
// 1. 獲取總筆數
$total = $pdo->query('SELECT COUNT(*) FROM articles')->fetchColumn();
// 2. 計算總頁數
$totalPages = ceil($total / $pageSize);
$page = min($page, max(1, $totalPages)); // 修正頁碼範圍
// 3. 計算偏移量
$offset = ($page - 1) * $pageSize;
// 4. 查詢當前頁數據
$stmt = $pdo->prepare('SELECT * FROM articles ORDER BY created_at DESC LIMIT ?, ?');
$stmt->bindValue(1, $offset, PDO::PARAM_INT);
$stmt->bindValue(2, $pageSize, PDO::PARAM_INT);
$stmt->execute();
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<?php
function renderPagination(int $currentPage, int $totalPages, string $baseUrl = '?p='): string {
if ($totalPages <= 1) return '';
$html = '<nav aria-label="分頁"><ul class="pagination">';
// 首頁/上一頁
if ($currentPage > 1) {
$html .= '<li><a href="' . $baseUrl . '1">首頁</a></li>';
$html .= '<li><a href="' . $baseUrl . ($currentPage - 1) . '">上一頁</a></li>';
}
// 頁碼範圍計算 (顯示當前頁前後各 2 頁)
$start = max(1, $currentPage - 2);
$end = min($totalPages, $currentPage + 2);
// 調整範圍保持 5 個頁碼
if ($end - $start < 4) {
if ($start == 1) $end = min(5, $totalPages);
if ($end == $totalPages) $start = max(1, $totalPages - 4);
}
for ($i = $start; $i <= $end; $i++) {
$class = $i === $currentPage ? ' class="active"' : '';
$html .= "<li$class><a href=\"{$baseUrl}$i\">$i</a></li>";
}
// 下一頁/尾頁
if ($currentPage < $totalPages) {
$html .= '<li><a href="' . $baseUrl . ($currentPage + 1) . '">下一頁</a></li>';
$html .= '<li><a href="' . $baseUrl . $totalPages . '">尾頁</a></li>';
}
$html .= '</ul></nav>';
return $html;
}
?>
後端 API:api/articles.php
<?php
header('Content-Type: application/json');
$page = max(1, (int)($_GET['page'] ?? 1));
$pageSize = 10;
// ... 查詢邏輯同上 ...
echo json_encode([
'code' => 200,
'data' => $items,
'pagination' => [
'current_page' => $page,
'total_pages' => $totalPages,
'total_items' => $total,
'page_size' => $pageSize
]
]);
?>
前端 JS (原生 Fetch)
<script>
let currentPage = 1;
const container = document.getElementById('article-list');
const pagination = document.getElementById('pagination');
async function loadPage(page) {
currentPage = page;
const resp = await fetch(`api/articles.php?page=${page}`);
const data = await resp.json();
if (data.code === 200) {
renderArticles(data.data);
renderPagination(data.pagination);
}
}
function renderArticles(items) {
container.innerHTML = items.map(item => `
<article>
<h3>${escapeHtml(item.title)}</h3>
<p>${escapeHtml(item.excerpt)}</p>
</article>
`).join('');
}
function renderPagination(pager) {
let html = '';
if (pager.current_page > 1) {
html += `<button onclick="loadPage(${pager.current_page - 1})">上一頁</button>`;
}
for (let i = 1; i <= pager.total_pages; i++) {
const active = i === pager.current_page ? ' class="active"' : '';
html += `<button${active} onclick="loadPage(${i})">${i}</button>`;
}
if (pager.current_page < pager.total_pages) {
html += `<button onclick="loadPage(${pager.current_page + 1})">下一頁</button>`;
}
pagination.innerHTML = html;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 初始載入
loadPage(1);
</script>
<script>
let loading = false;
let page = 1;
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading) {
loading = true;
page++;
const resp = await fetch(`api/articles.php?page=${page}`);
const data = await resp.json();
if (data.code === 200 && data.data.length > 0) {
data.data.forEach(item => {
const el = document.createElement('article');
el.innerHTML = `<h3>${escapeHtml(item.title)}</h3><p>${escapeHtml(item.excerpt)}</p>`;
container.appendChild(el);
});
} else {
observer.disconnect(); // 沒有更多
}
loading = false;
}
});
observer.observe(document.getElementById('load-trigger'));
</script>
<div id="load-trigger" style="height: 1px;"></div>
| 優化點 | 方案 |
|---|---|
| 大偏移量查詢慢 | 游標分頁 WHERE id > ? ORDER BY id LIMIT ? (需唯一遞增鍵) |
| COUNT(*) 慢 | 快取總數、近似值、觸發器維護計數表 |
| 重複渲染 | 前端虛擬列表、Vue/React 虛擬 DOM |
| SEO 支援 | SSR 渲染首屏、預渲染、動態渲染服務 |
分頁看似簡單,實則涉及資料庫優化、前端體驗、SEO 平衡等多維考量,根據業務場景選擇傳統分頁、AJAX 分頁或無限滾動。