靜態化是高流量網站(門戶、電商詳情頁、文章頁)核心優化手段。PHP 生成靜態 HTML 主要有三大流派:模板引擎直接寫檔、輸出緩衝捕獲輸出、佇列異步生成。本文深度對比三種方案原理、代碼實現、適用場景與避坑指南。
file_put_contents() 寫入靜態檔<?php
/**
* 原生 PHP 模板渲染生成靜態頁
* @param string $templatePath 模板檔路徑(包含 語法)
* @param array $data 注入變量
* @param string $outputPath 輸出靜態檔路徑
*/
function renderStaticNative(string $templatePath, array $data, string $outputPath): bool {
// 1. 解析數據到區域變量(extract 便於模板直接用 $var)
extract($data, EXTR_SKIP);
// 2. 開啟輸出緩衝捕獲模板輸出
ob_start();
try {
include $templatePath; // 模板內可直接使用 $title, $content 等
$html = ob_get_clean();
} catch (Throwable $e) {
ob_end_clean();
error_log("Template render error: {$e->getMessage()}");
return false;
}
// 3. 確保輸出目錄存在
$dir = dirname($outputPath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// 4. 原子寫入(避免讀取到半寫入檔案)
$tmpPath = $outputPath . '.tmp.' . random_bytes(8);
if (file_put_contents($tmpPath, $html) === false) {
return false;
}
// rename 在同一檔案系統上是原子操作
return rename($tmpPath, $outputPath);
}
// 模板檔:templates/article.php
//
//
// = htmlspecialchars($title) ?>
//
// = htmlspecialchars($title) ?>
// = $content ?>
//
//
//
// 使用
$data = [
'title' => 'PHP 靜態化實戰',
'content' => '內容...
',
'publishTime' => time(),
];
renderStaticNative(__DIR__ . '/templates/article.php', $data, '/var/www/html/article/123.html');
?>
<?php
require 'vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/templates');
$twig = new \Twig\Environment($loader, [
'cache' => __DIR__ . '/cache/twig', // 編譯快取目錄
'autoescape' => 'html', // 預設 HTML 轉義
'strict_variables' => true,
]);
function renderStaticTwig(\Twig\Environment $twig, string $template, array $data, string $outputPath): bool {
try {
$html = $twig->render($template, $data);
} catch (\Twig\Error\LoaderError|\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) {
error_log("Twig render error: {$e->getMessage()}");
return false;
}
$dir = dirname($outputPath);
if (!is_dir($dir)) mkdir($dir, 0755, true);
$tmpPath = $outputPath . '.tmp.' . bin2hex(random_bytes(8));
if (file_put_contents($tmpPath, $html) === false) return false;
return rename($tmpPath, $outputPath);
}
// 使用
renderStaticTwig($twig, 'article.twig', $data, '/var/www/html/article/123.html');
?>
直接請求動態頁面 URL(或內部 include),用 ob_start() 捕獲完整 HTML 輸出,寫檔。適合「動態頁已寫好,需快速靜態化」場景。
<?php
/**
* 通過輸出緩衝生成靜態頁(支援 URL 或檔案包含)
*/
function renderStaticByOb(string $source, string $outputPath, bool $isUrl = false): bool {
if ($isUrl) {
// 方式 A:cURL 獲取完整頁面(含 Cookie、Session、重定向)
$ch = curl_init($source);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_COOKIEJAR => '', // 不保存 Cookie
CURLOPT_SSL_VERIFYPEER => false, // 內網可關閉
CURLOPT_TIMEOUT => 30,
]);
$html = curl_exec($ch);
if ($html === false) {
error_log("cURL error: " . curl_error($ch));
curl_close($ch);
return false;
}
curl_close($ch);
} else {
// 方式 B:直接 include 當前腳本上下文(變量共享,快)
ob_start();
try {
include $source;
$html = ob_get_clean();
} catch (Throwable $e) {
ob_end_clean();
error_log("Include render error: {$e->getMessage()}");
return false;
}
}
// 寫檔(同方案一)
$dir = dirname($outputPath);
if (!is_dir($dir)) mkdir($dir, 0755, true);
$tmpPath = $outputPath . '.tmp.' . bin2hex(random_bytes(8));
if (file_put_contents($tmpPath, $html) === false) return false;
return rename($tmpPath, $outputPath);
}
// 使用 A:靜態化遠端/本地 URL
renderStaticByOb('https://example.com/article/123', '/var/www/html/article/123.html', true);
// 使用 B:靜態化本地動態頁(需在動態頁入口判斷是否靜態化模式)
// article.php 入口:
// if (isset($_GET['static_export'])) {
// renderStaticByOb(__FILE__, '/var/www/html/article/123.html');
// exit;
// }
?>
若動態頁內部已使用輸出緩衝(如框架視圖渲染),外層 ob_start() 會巢狀。解決:
ob_get_level() > 0 時用 ob_get_contents() 而非 ob_get_clean()
[內容變更事件] → [寫入靜態化任務佇列] → [Worker 消費] → [渲染模板] → [原子寫檔] → [CDN 刷新/預熱]
↑ ↑
└────────────────────── [失敗重試、死信佇列、監控告警] ←──────────────────┘
CREATE TABLE static_tasks (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(50) NOT NULL, -- 'article', 'product', 'category', 'sitemap'
target_id BIGINT UNSIGNED NOT NULL, -- 文章 ID、商品 ID 等
template VARCHAR(100), -- 指定模板(可選)
priority TINYINT UNSIGNED DEFAULT 10,
status ENUM('pending','processing','completed','failed') DEFAULT 'pending',
attempts TINYINT UNSIGNED DEFAULT 0,
max_attempts TINYINT UNSIGNED DEFAULT 3,
last_error TEXT,
scheduled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_task (type, target_id, status), -- 防重複排隊
INDEX idx_status_priority (status, priority, scheduled_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
class StaticTaskProducer {
private \PDO $db;
public function __construct(\PDO $db) { $this->db = $db; }
public function enqueue(string $type, int $targetId, int $priority = 10, ?string $template = null): void {
$stmt = $this->db->prepare(
"INSERT IGNORE INTO static_tasks (type, target_id, template, priority, scheduled_at)
VALUES (?, ?, ?, ?, NOW())"
);
$stmt->execute([$type, $targetId, $template, $priority]);
}
}
// 文章發布後
$producer = new StaticTaskProducer($pdo);
$producer->enqueue('article', $articleId, 5); // 高優先級
?>
<?php
class StaticTaskWorker {
private \PDO $db;
private \Twig\Environment $twig;
private string $outputBase;
public function __construct(\PDO $db, \Twig\Environment $twig, string $outputBase) {
$this->db = $db;
$this->twig = $twig;
$this->outputBase = $outputBase;
}
public function runOnce(): bool {
$stmt = $this->db->prepare(
"SELECT * FROM static_tasks
WHERE status = 'pending' AND scheduled_at <= NOW()
ORDER BY priority ASC, id ASC
LIMIT 1 FOR UPDATE SKIP LOCKED"
);
$stmt->execute();
$task = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$task) return false;
$this->markProcessing($task['id']);
try {
$data = $this->fetchData($task['type'], $task['target_id']);
$template = $task['template'] ?? "{$task['type']}.twig";
$outputPath = $this->resolvePath($task['type'], $task['target_id']);
$html = $this->twig->render($template, $data);
$this->atomicWrite($outputPath, $html);
// 可選:CDN 刷新/預熱
$this->purgeCdn($outputPath);
$this->markCompleted($task['id']);
} catch (Throwable $e) {
$this->markFailed($task['id'], $e->getMessage());
}
return true;
}
private function fetchData(string $type, int $id): array {
// 根據類型查詢數據,返回模板所需變量陣列
$tableMap = ['article' => 'articles', 'product' => 'products'];
$stmt = $this->db->prepare("SELECT * FROM {$tableMap[$type]} WHERE id=?");
$stmt->execute([$id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) throw new \RuntimeException("Data not found: $type $id");
return $row;
}
private function resolvePath(string $type, int $id): string {
return $this->outputBase . "/$type/$id.html";
}
private function atomicWrite(string $path, string $content): void {
$dir = dirname($path);
if (!is_dir($dir)) mkdir($dir, 0755, true);
$tmp = $path . '.tmp.' . bin2hex(random_bytes(8));
if (file_put_contents($tmp, $content) === false) throw new \RuntimeException("Write failed: $path");
if (!rename($tmp, $path)) throw new \RuntimeException("Rename failed: $path");
}
private function purgeCdn(string $path): void {
// 對接 CDN API(阿里雲、騰訊雲、Cloudflare、Fastly)
// 將相對路徑轉為 URL 提交刷新
}
private function markProcessing(int $id): void {
$this->db->prepare("UPDATE static_tasks SET status='processing', attempts=attempts+1 WHERE id=?")->execute([$id]);
}
private function markCompleted(int $id): void {
$this->db->prepare("UPDATE static_tasks SET status='completed', completed_at=NOW() WHERE id=?")->execute([$id]);
}
private function markFailed(int $id, string $error): void {
$this->db->prepare("UPDATE static_tasks SET status='failed', last_error=? WHERE id=?")->execute([$error, $id]);
}
}
// CLI 長駐進程(Supervisor 管理)
$worker = new StaticTaskWorker($pdo, $twig, '/var/www/html');
while (true) {
if (!$worker->runOnce()) sleep(1);
}
?>
| 維度 | 模板引擎直寫 | 輸出緩衝捕獲 | 佇列異步 |
|---|---|---|---|
| 改造成本 | 中(需編寫模板) | 低(復用現有頁面) | 高(需佇列基礎設施) |
| 效能影響 | 同步阻塞請求 | 同步阻塞請求 | 完全異步,不影響主流程 |
| 並發能力 | 受限於 PHP-FPM 進程數 | 同 | 水平擴展 Worker 數量 |
| 重試/失敗處理 | 需自行實現 | 需自行實現 | 內建重試、死信、告警 |
| CDN 聯動 | 同步呼叫 | 同步呼叫 | 解耦、可批次、可延遲 |
| 適用場景 | 新專案、模板可控、中小流量 | 舊專案遷移、快速上線、低頻頁面 | 高併發、大流量、核心業務頁面 |
{% include 'partials/header.twig' with {cache_key: 'header_v2'} %}。article_123_v5.html,Nginx try_files 回退舊版,支援秒級回滾。SETNX lock:static:article:123 10 確保同一頁面同一時間只有一個 Worker 生成。PHP 靜態化三大方案各有甜點:模板引擎直寫最標準、可控;輸出緩衝最快遷移舊系統;佇列異步是高併發架構標配。建議:新專案直上佇列異步 + Twig/Blade 模板,舊專案先用輸出緩衝過渡,逐步重構模板並遷移至佇列。核心原則:原子寫檔、模板與數據分離、失敗可重試、CDN 聯動自動化。