將動態用戶資料批量生成靜態 HTML 頁面(如 user/user-123.html),降低資料庫壓力、提升前端載入速度、利於 SEO。
<?php
// 原始碼主要問題:
// 1. fopen('moban.html', 'r') 放在循環外,但 fclose($fp) 在循環內 → 第二次迴代報錯
// 2. $str = fread(...) 只讀一次,但循環中反覆 str_replace 會累積替換(第二輪在已替換的字串上再替換)
// 3. 逐用戶查詢資料庫(N+1 問題),極慢
// 4. 無錯誤處理、無併發控制、無進度顯示
?>
<?php
// config.php
return [
'db' => [
'dsn' => 'mysql:host=127.0.0.1;dbname=cms;charset=utf8mb4',
'user' => 'root',
'pass' => 'password',
'options' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
],
'template' => __DIR__ . '/templates/user_detail.tpl',
'outputDir' => __DIR__ . '/public/user/',
'batchSize' => 100, // 分批處理,避免記憶體溢出
];
// generate.php
require 'vendor/autoload.php';
$config = require 'config.php';
$pdo = new PDO($config['db']['dsn'], $config['db']['user'], $config['db']['pass'], $config['db']['options']);
// 1. 一次性讀取模板
$template = file_get_contents($config['template']);
if ($template === false) {
die("模板文件讀取失敗: {$config['template']}");
}
// 2. 確保輸出目錄存在
if (!is_dir($config['outputDir'])) {
mkdir($config['outputDir'], 0755, true);
}
// 3. 分批查詢用戶 ID(游標/生成器模式更佳,這裡用 LIMIT/OFFSET 示意)
$page = 1;
$pageSize = $config['batchSize'];
$totalGenerated = 0;
$startTime = microtime(true);
while (true) {
$stmt = $pdo->prepare('SELECT uid, username, email, reg_time, profile FROM users LIMIT ? OFFSET ?');
$stmt->execute([$pageSize, ($page - 1) * $pageSize]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($users)) break; // 無更多資料
foreach ($users as $user) {
// 4. 每次基於原始模板替換(避免累積替換)
$html = $template;
$html = str_replace(
['{username}', '{email}', '{reg_time}', '{profile}'],
[
htmlspecialchars($user['username']),
htmlspecialchars($user['email']),
date('Y-m-d H:i:s', strtotime($user['reg_time'])),
nl2br(htmlspecialchars($user['profile']))
],
$html
);
// 5. 寫入靜態文件(原子寫入:寫臨時文件 -> rename)
$fileName = "user-{$user['uid']}.html";
$filePath = $config['outputDir'] . $fileName;
$tmpPath = $filePath . '.tmp';
if (file_put_contents($tmpPath, $html) === false) {
error_log("寫入失敗: $filePath");
continue;
}
if (!rename($tmpPath, $filePath)) {
error_log("重命名失敗: $filePath");
@unlink($tmpPath);
continue;
}
$totalGenerated++;
}
$page++;
// 可選:釋放大變數
unset($users);
gc_collect_cycles();
}
$elapsed = microtime(true) - $startTime;
echo "生成完成!共 $totalGenerated 頁,耗時 " . number_format($elapsed, 3) . " 秒\n";
?>
| 優化項 | 原始問題 | 解決方案 |
|---|---|---|
| 模板讀取 | 循環內重複讀取/關閉 | 循環外 file_get_contents 一次讀取 |
| 替換邏輯 | 累積替換導致錯誤 | 每次 $html = $template 重置,使用陣列 str_replace 一次性替換 |
| 資料庫查詢 | N+1 查詢 | 分批 SELECT ... LIMIT/OFFSET 或游標 PDO::FETCH_ORI_NEXT |
| 檔案寫入 | 直接寫入可能產生殘檔 | 臨時檔 .tmp + rename() 原子操作 |
| XSS 防護 | 無轉義 | htmlspecialchars() + nl2br() 安全輸出 |
| 記憶體控制 | 大量用戶撐爆記憶體 | 分批處理 + unset() + gc_collect_cycles() |
<?php
// 使用 Symfony Process 或 pcntl_fork 併發
// 將用戶 ID 陣列切分為 N 份,啟動 N 個 Worker 進程並行生成
// 主進程監控進度、收集錯誤、合併日誌
// 注意:資料庫連線數、磁碟 I/O 瓶頸、檔案系統 inode 限制
?>
try_files /user/user-$uid.html @php; 靜態檔存在直接服務,不存在回落 PHP 動態生成(或 404)。批量靜態化核心在於 「模板一次讀、資料分批取、每輪重置替換、原子寫入檔」。結合分批處理、佇列異步、CDN 分發,即可構建高效能、可擴展的靜態站點架構。