核心原理
- 依請求 URI 產生唯一快取檔名(MD5)
- 檢查快取檔是否存在且未過期 → 直接輸出內容並結束腳本
- 否則啟動輸出緩衝,註冊關閉回調函式寫入快取
- 非 GET 請求(POST/PUT/DELETE)自動刪除對應快取
- 定期清理過期快取檔案
完整實作
<?php
// cache.php - 放置於專案入口檔案最開頭 require_once
define('CACHE_ROOT', __DIR__ . '/cache');
define('CACHE_LIFETIME', 1800); // 秒
define('CACHE_EXT', '.html');
$uri = $_SERVER['REQUEST_URI'];
$cacheName = md5($uri) . CACHE_EXT;
$cacheDir = CACHE_ROOT . DIRECTORY_SEPARATOR . $cacheName[0];
$cacheFile = $cacheDir . DIRECTORY_SEPARATOR . $cacheName;
// 僅快取 GET 請求
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// 命中且未過期
if (is_file($cacheFile) && (time() - filemtime($cacheFile) < CACHE_LIFETIME)) {
readfile($cacheFile);
exit;
}
// 建立目錄
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
// 關閉回調寫入快取
function writeCache($content) {
global $cacheFile;
$fp = fopen($cacheFile, 'wb');
fwrite($fp, $content);
fclose($fp);
@chmod($cacheFile, 0644);
// 可選:同步清理過期檔案
// cleanOldCache();
return $content;
}
ob_start('writeCache');
} else {
// 非 GET 請求刪除快取
if (is_file($cacheFile)) @unlink($cacheFile);
}
function cleanOldCache() {
$dir = new RecursiveDirectoryIterator(CACHE_ROOT);
$iter = new RecursiveIteratorIterator($dir);
foreach ($iter as $file) {
if ($file->isFile() && $file->getExtension() === 'html') {
if (time() - $file->getMTime() > CACHE_LIFETIME) {
@unlink($file->getPathname());
}
}
}
}
?>
使用方式
<?php
// index.php
require_once 'cache.php';
// 後續正常業務邏輯
echo 'Hello World'; // 將被快取
?>
進階擴充
- 分組快取:依功能模組建立子目錄,支援整組失效
- 標籤快取:維護
tag -> [cache_files] 對應,精準清理
- 記憶體快取:搭配 APCu/Redis 存儲熱點資料,檔案快取作為二級
- 壓縮輸出:
ob_start(function($c) { return gzencode($c, 9); }) 並發送 Content-Encoding: gzip
注意事項
- 動態內容(用戶資訊、購物車、CSRF Token)不可快取,需採用片段快取或 JS 渲染
- 快取目錄需設定正確權限,避免權限錯誤導致寫入失敗
- 高併發下可能產生「快取擊穿」,建議加入鎖機制或使用 Redis 原子操作