首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP + AJAX 無刷新分頁完整實戰:從數據庫到前端交互的現代化實現

     編輯:PHP 綜合知識

架構概覽

採用 **後端渲染 HTML 片段 + 前端替換容器** 模式 (而非純 JSON + 前端模板),優勢:SEO 友好、模板復用、邏輯集中、開發效率高。

一、數據庫準備

CREATE TABLE `tb_user` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL DEFAULT '',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `tb_user` (`username`) VALUES
('Alice'), ('Bob'), ('Charlie'), ('David'), ('Eve'),
('Frank'), ('Grace'), ('Heidi'), ('Ivan'), ('Judy');

二、後端 API (index.php) - 統一入口處理列表+分頁

<?php
// index.php
require_once __DIR__ . '/config/db.php'; // PDO 連接

$page     = max(1, (int)($_GET['page'] ?? 1));
$pageSize = 3; // 每頁條數
$offset   = ($page - 1) * $pageSize;

// 總數查詢 (可緩存)
$totalStmt = $pdo->query('SELECT COUNT(*) FROM tb_user');
$total     = (int)$totalStmt->fetchColumn();
$pageCount = max(1, (int)ceil($total / $pageSize));
$page      = min($page, $pageCount);

// 數據查詢
$dataStmt = $pdo->prepare('SELECT id, username, created_at FROM tb_user ORDER BY id DESC LIMIT :offset, :limit');
$dataStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$dataStmt->bindValue(':limit', $pageSize, PDO::PARAM_INT);
$dataStmt->execute();
$users = $dataStmt->fetchAll(PDO::FETCH_ASSOC);

// 分頁導航構建
$nav = '';
$nav .= "<span class='page-info'>共 {$total} 條 / {$pageCount} 頁</span> ";

$prevPage = $page > 1 ? $page - 1 : 1;
$nextPage = $page < $pageCount ? $page + 1 : $pageCount;

$nav .= "<a href='?page=1' class='page-btn' data-page='1'>首頁</a> ";
$nav .= "<a href='?page={$prevPage}' class='page-btn' data-page='{$prevPage}'>上一頁</a> ";
$nav .= "<a href='?page={$nextPage}' class='page-btn' data-page='{$nextPage}'>下一頁</a> ";
$nav .= "<a href='?page={$pageCount}' class='page-btn' data-page='{$pageCount}'>尾頁</a> ";

// AJAX 請求只返回片段;普通請求返回完整頁面
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';

if ($isAjax) {
    header('Content-Type: text/html; charset=utf-8');
    // 僅輸出表格 + 分頁 (無外層容器)
    include __DIR__ . '/templates/user_list_partial.php';
    exit;
}

// 完整頁面
include __DIR__ . '/templates/user_list_full.php';

三、模板文件

1. 完整頁面模板 (templates/user_list_full.php)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>用戶列表 - AJAX 分頁演示</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
        table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
        th, td { padding: 0.75rem; border: 1px solid #e0e0e0; text-align: left; }
        th { background: #f5f5f5; }
        .pagination { margin-top: 1.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
        .page-btn { padding: 0.4rem 0.8rem; border: 1px solid #ccc; background: #fff; border-radius: 4px; text-decoration: none; color: #333; cursor: pointer; transition: all .2s; }
        .page-btn:hover:not(.disabled) { background: #007bff; color: #fff; border-color: #007bff; }
        .page-btn.disabled { pointer-events: none; opacity: 0.5; }
        .page-info { color: #666; font-size: 0.9rem; }
        .loading { color: #999; font-style: italic; }
    </style>
</head>
<body>
    <h1>用戶列表 (AJAX 無刷新分頁)</h1>

    <!-- 核心容器:AJAX 將替換此處內容 -->
    <div id="user-list-container">
        <?php include __DIR__ . '/user_list_partial.php'; ?>
    </div>

    <script src="assets/js/pagination.js"></script>
</body>
</html>

2. 列表片段模板 (templates/user_list_partial.php)

<table id="user-table">
    <thead>
        <tr><th>ID</th><th>用戶名</th><th>註冊時間</th></tr>
    </thead>
    <tbody>
        <?php foreach ($users as $user): ?>
        <tr>
            <td><?= $user['id'] ?></td>
            <td><?= htmlspecialchars($user['username']) ?></td>
            <td><?= $user['created_at'] ?></td>
        </tr>
        <?php endforeach; ?>
        <?php if (empty($users)): ?>
        <tr><td colspan="3" style="text-align:center;">暫無數據</td></tr>
        <?php endif; ?>
    </tbody>
</table>

<div class="pagination">
    <?= $nav ?>
</div>

四、前端交互腳本 (assets/js/pagination.js) - 原生 JS、無依賴

// pagination.js
(function () {
    'use strict';

    const container = document.getElementById('user-list-container');
    if (!container) return;

    // 事件代理:處理動態生成的分頁鏈接
    container.addEventListener('click', function (e) {
        const link = e.target.closest('.page-btn');
        if (!link) return;

        e.preventDefault();
        const page = link.dataset.page;
        if (!page) return;

        loadPage(page);
    });

    // 支持瀏覽器前進/後退
    window.addEventListener('popstate', function (e) {
        if (e.state && e.state.page) {
            loadPage(e.state.page, false); // 不再 pushState
        }
    });

    function loadPage(page, pushState = true) {
        // 1. 顯示加載狀態
        container.innerHTML = '<div class="loading">正在加載第 ' + page + ' 頁...</div>';

        // 2. 發送 AJAX 請求 (帶 X-Requested-With 標頭)
        fetch('?page=' + encodeURIComponent(page), {
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'Accept': 'text/html'
            },
            credentials: 'same-origin' // 攜帶 Cookie/Session
        })
        .then(response => {
            if (!response.ok) throw new Error('HTTP ' + response.status);
            return response.text();
        })
        .then(html => {
            // 3. 替換容器內容
            container.innerHTML = html;

            // 4. 更新 URL (不刷新)
            if (pushState) {
                const url = new URL(window.location);
                url.searchParams.set('page', page);
                history.pushState({ page }, '', url);
            }

            // 5. 滾動到頂部 (可選)
            // container.scrollIntoView({ behavior: 'smooth' });
        })
        .catch(err => {
            console.error(err);
            container.innerHTML = '<div style="color:red; padding:1rem;">加載失敗:' + err.message + '</div>';
        });
    }

    // 初始化:高亮當前頁按鈕 (可選)
    function highlightCurrentPage() {
        const currentPage = new URLSearchParams(window.location.search).get('page') || '1';
        container.querySelectorAll('.page-btn').forEach(btn => {
            btn.classList.toggle('disabled', btn.dataset.page === currentPage);
        });
    }
    highlightCurrentPage();
})();

五、進階優化:防抖/緩存/預加載

<?php
// 在 pagination.js 中擴展

// 1. 簡單內存緩存 (避免重複請求同一頁)
const pageCache = new Map();

function loadPage(page, pushState = true) {
    if (pageCache.has(page)) {
        container.innerHTML = pageCache.get(page);
        if (pushState) updateUrl(page);
        highlightCurrentPage();
        return;
    }
    // ... fetch 後
    .then(html => {
        pageCache.set(page, html); // 緩存
        container.innerHTML = html;
        // ...
    });
}

// 2. 預加載鄰近頁 (鼠標懸停預取)
container.addEventListener('mouseover', function (e) {
    const link = e.target.closest('.page-btn');
    if (link && !pageCache.has(link.dataset.page)) {
        // 低優先級預取
        fetch('?page=' + link.dataset.page, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, priority: 'low' })
            .then(r => r.text())
            .then(html => pageCache.set(link.dataset.page, html));
    }
});

// 3. 防抖快速點擊 (可選)
let lastClick = 0;
container.addEventListener('click', function (e) {
    const link = e.target.closest('.page-btn');
    if (link && Date.now() - lastClick < 300) e.preventDefault(); // 300ms 內忽略
    lastClick = Date.now();
});

六、關鍵點總結與避坑指南

環節關鍵點常見錯誤
SQL使用 `LIMIT :offset, :limit` 參數綁定;總數查詢可緩存字符串拼接 SQL 注入;`OFFSET` 大時性能急劇下降 (建議游標分頁)
後端識別 AJAX`$_SERVER['HTTP_X_REQUESTED_WITH'] === 'xmlhttprequest'` (jQuery/axios/fetch 默認發送)未發送標頭導致返回完整頁面;原生 fetch 需手動添加
模板分離片段模板**無外層容器**,直接輸出表格+分頁片段包含 `` 導致嵌套非法結構
前端替換使用 `innerHTML` 替換**容器內部**;事件代理處理動態元素`outerHTML` 替換容器自身導致引用丟失;重複綁定事件
URL 同步`history.pushState` 更新地址欄,支持刷新/分享/後退不更新 URL 導致刷新回第一頁;`replaceState` 覆蓋歷史
錯誤處理統一返回 4xx/5xx + 友好提示;前端 catch 顯示PHP 致命錯誤輸出 HTML 破壞片段結構
XSS 防護輸出變量**必須** `htmlspecialchars()`直接輸出用戶輸入導致存儲型 XSS

現代化替代方案對比

  • Alpine.js / htmx:聲明式,極少 JS,`hx-get="?page=2" hx-target="#container"` 一行搞定。
  • Vue/React + API (JSON):前後端分離,首屏 SSR/SSG,組件化復用,適合複雜交互。
  • Livewire / Laravel Livewire:後端驅動前端,無需寫 JS,PHP 狀態自動同步。

選型建議: 簡單列表分頁 → **原生 fetch + 後端片段渲染** (本文方案);中等複雜度 → **Alpine.js/htmx**;SPA/富交互 → **Vue/React + JSON API**;Laravel 團隊 → **Livewire**。核心原則:模板復用、邏輯集中、漸進增強。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved