無刷新分頁能顯著提升列表頁體驗,減少整頁重渲染開銷。本文從數據庫建表、原生 JS 封裝 AJAX、PHP 後端分頁邏輯、前端渲染四個維度,提供一套可直接落地的完整代碼,並對比 jQuery/Fetch 現代寫法。
CREATE TABLE `tb_user` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tb_user` (`username`) VALUES
('aaa'), ('bbb'), ('ccc'), ('ddd'), ('eee'),
('fff'), ('ggg'), ('hhh'), ('用戶9'), ('用戶10');
/**
* 簡易 AJAX 分頁工具
* 依賴:無(原生 XMLHttpRequest)
* 用法:dopage('容器ID', '請求URL')
*/
var httpRequest = false;
var reobj = ''; // 目標容器 ID
function sendRequest(url) {
httpRequest = false;
// 兼容創建 XHR
if (window.XMLHttpRequest) {
httpRequest = new XMLHttpRequest();
if (httpRequest.overrideMimeType) {
httpRequest.overrideMimeType('text/html');
}
} else if (window.ActiveXObject) { // IE6-
try { httpRequest = new ActiveXObject('Msxml2.XMLHTTP'); }
catch (e) { try { httpRequest = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} }
}
if (!httpRequest) {
alert('無法創建 XMLHttpRequest 對象');
return false;
}
httpRequest.onreadystatechange = processRequest;
httpRequest.open('GET', url, true);
httpRequest.send(null);
}
function processRequest() {
if (httpRequest.readyState === 4) {
if (httpRequest.status === 200) {
document.getElementById(reobj).innerHTML = httpRequest.responseText;
} else {
alert('請求失敗:' + httpRequest.status);
}
}
}
function dopage(obj, url) {
document.getElementById(obj).innerHTML = '正在加載...';
reobj = obj;
sendRequest(url);
}
<?php
// 配置
$pageSize = 3;
$host = 'localhost'; $user = 'root'; $pass = ''; $dbname = 'test';
$mysqli = new mysqli($host, $user, $pass, $dbname);
if ($mysqli->connect_errno) die('DB連接失敗: ' . $mysqli->connect_error);
$mysqli->set_charset('utf8mb4');
// 參數
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$url = 'index.php'; // 當前頁面 URL
// 總記錄數
$totalRes = $mysqli->query("SELECT COUNT(*) AS cnt FROM tb_user");
$total = $totalRes->fetch_assoc()['cnt'];
$totalRes->free();
$pageCount = ceil($total / $pageSize);
$page = min($page, $pageCount);
$offset = ($page - 1) * $pageSize;
// 查詢數據
$dataRes = $mysqli->query("SELECT * FROM tb_user ORDER BY id DESC LIMIT $offset, $pageSize");
$rows = $dataRes->fetch_all(MYSQLI_ASSOC);
$dataRes->free();
$mysqli->close();
// 生成分頁導航 HTML
$pagenav = '';
$pagenav .= "顯示第 " . ($total ? $offset+1 : 0) . "-" . min($offset+$pageSize, $total) . " 條,共 $total 條,第 $page 頁 / 共 $pageCount 頁 ";
$prev = $page > 1 ? $page - 1 : 0;
$next = $page < $pageCount ? $page + 1 : 0;
$pagenav .= $prev ? "上一頁 " : "上一頁 ";
$pagenav .= $next ? "下一頁 " : "下一頁 ";
$pagenav .= $pageCount ? "尾頁" : "";
?>
AJAX 分頁演示
ID 用戶名
= $row['id'] ?>
= htmlspecialchars($row['username']) ?>
= $pagenav ?>
後端 API (api.php):返回 JSON,分離關注點。
<?php
header('Content-Type: application/json; charset=utf-8');
$mysqli = new mysqli('localhost', 'root', '', 'test');
$page = max(1, intval($_GET['page'] ?? 1));
$pageSize = 3;
$offset = ($page - 1) * $pageSize;
$total = $mysqli->query("SELECT COUNT(*) c FROM tb_user")->fetch_row()[0];
$res = $mysqli->query("SELECT id, username FROM tb_user ORDER BY id DESC LIMIT $offset, $pageSize");
$data = $res->fetch_all(MYSQLI_ASSOC);
echo json_encode([
'page' => $page,
'pageSize' => $pageSize,
'total' => $total,
'pageCount' => ceil($total / $pageSize),
'data' => $data
], JSON_UNESCAPED_UNICODE);
前端渲染 (app.js):
async function loadPage(page) {
const container = document.getElementById('result');
container.innerHTML = '加載中...';
try {
const resp = await fetch(`api.php?page=${page}`);
const json = await resp.json();
renderTable(json.data);
renderPager(json);
} catch (e) {
container.innerHTML = '加載失敗: ' + e.message;
}
}
function renderTable(rows) {
let html = 'ID 用戶名 ';
rows.forEach(r => html += `${r.id} ${escapeHtml(r.username)} `);
html += '
';
document.getElementById('tableWrap').innerHTML = html;
}
function renderPager({page, pageCount, total}) {
let html = `顯示第 ${(page-1)*3+1}-${Math.min(page*3, total)} 條,共 ${total} 條,第 ${page}/${pageCount} 頁 `;
if (page > 1) html += ` `;
if (page < pageCount) html += ` `;
document.getElementById('pagerWrap').innerHTML = html;
}
function escapeHtml(str) {
return str.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
// 初始化
loadPage(1);
| 問題 | 解決方案 |
|---|---|
| 全選/反選失效 | 動態插入的 checkbox 需事件委託:document.getElementById('result').addEventListener('change', e => { if(e.target.matches('.chk')) ... }),或渲染後重新綁定。 |
| 瀏覽器前進/後退不恢復分頁狀態 | 使用 History API:history.pushState({page}, '', `?page=${page}`),監聽 popstate 事件恢復。 |
| 併發請求導致頁碼亂序 | 加鎖標誌位 let loading = false,請求前檢查,完成後釋放。 |
| SEO 不友好 | 服務端渲染首屏(如上方 PHP 直出),AJAX 只負責翻頁;或使用 Nuxt/Next.js 同構方案。 |