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

PHP header() 圖片輸出與瀏覽器緩存控制:Last-Modified / ETag / Cache-Control 完整實戰

     編輯:PHP 相關知識

直接用 PHP 輸出圖片(如驗證碼、水印圖、動態圖表)時,若不控制緩存,每次請求都會觸發 PHP 執行,消耗 CPU 與帶寬。本文系統講解 Cache-Control、Expires、Last-Modified、ETag 四大緩存頭部的協作機制,提供可直接生產使用的綜合範例。

HTTP 緩存頭部速覽表

頭部類型作用優先級
Cache-Control強緩存指定 max-age、private/public、no-store 等指令最高
Expires強緩存HTTP/1.0 兼容,絕對過期時間(GMT)次之
Last-Modified協商緩存資源最後修改時間,配合 If-Modified-Since較低
ETag協商緩存資源指紋(哈希/版本號),配合 If-None-Match,強於 Last-Modified最低

完整輸出範例:動態縮圖 + 完整緩存策略

<?php
// ================= 1. 緩存頭部(必須在任何輸出前) =================
$cacheSeconds = 86400 * 2; // 2 天
$etag = '"' . md5(__FILE__ . $imagePath) . '"'; // 基於文件路徑+腳本生成弱 ETag

// 強緩存:瀏覽器 2 天內不發請求
header("Cache-Control: private, max-age=$cacheSeconds, must-revalidate");
header("Expires: " . gmdate('D, d M Y H:i:s', time() + $cacheSeconds) . ' GMT');

// 協商緩存標識
header("Last-Modified: " . gmdate('D, d M Y H:i:s', filemtime($imagePath)) . ' GMT');
header("ETag: $etag");

// ================= 2. 處理協商緩存請求(304 Not Modified) =================
// 情況 A:瀏覽器發送 If-Modified-Since
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
    $clientTime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    $fileTime = filemtime($imagePath);
    if ($clientTime >= $fileTime) {
        header('HTTP/1.1 304 Not Modified');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $fileTime) . ' GMT');
        header("ETag: $etag");
        exit;
    }
}

// 情況 B:瀏覽器發送 If-None-Match(ETag 優先級更高)
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
    $clientEtag = trim($_SERVER['HTTP_IF_NONE_MATCH'], '"');
    if ($clientEtag === trim($etag, '"')) {
        header('HTTP/1.1 304 Not Modified');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($imagePath)) . ' GMT');
        header("ETag: $etag");
        exit;
    }
}

// ================= 3. 正常輸出圖片 =================
$imagePath = 'test.jpg'; // 來源圖片
$maxW = 200; $maxH = 200;

list($srcW, $srcH) = getimagesize($imagePath);
// 計算等比縮放尺寸
if ($srcW <= $maxW && $srcH <= $maxH) {
    $dstW = $srcW; $dstH = $srcH;
} elseif ($srcW / $maxW > $srcH / $maxH) {
    $dstW = $maxW;
    $dstH = (int)round($srcH * $maxW / $srcW);
} else {
    $dstH = $maxH;
    $dstW = (int)round($srcW * $maxH / $srcH);
}

$src = imagecreatefromjpeg($imagePath);
$dst = imagecreatetruecolor($dstW, $dstH);
imagecopyresampled($dst, $src, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);

header('Content-Type: image/jpeg');
header('Content-Length: ' . ob_get_length()); // 若開啟 OB
imagejpeg($dst, null, 90); // 直接輸出,質量 90

imagedestroy($src);
imagedestroy($dst);

關鍵邏輯解析

1. 強緩存優先

Cache-Control: max-age=172800 告訴瀏覽器「2 天內直接讀本地緩存,不發請求」。這是性能收益最大的一步。

2. 協商緩存兜底

強緩存過期後,瀏覽器會攜帶 If-Modified-Since 和/或 If-None-Match 發請求。服務端比對:

  • 文件修改時間未變 → 304 Not Modified(無 Body,極快)
  • 文件已變 → 正常輸出 200 + 新圖片 + 新緩存頭部

ETag 比 Last-Modified 更可靠:秒級修改、多服務器同步、文件內容未變但時間變等場景,ETag(基於內容哈希)不會誤判。

3. 私有 vs 共享緩存

  • private:僅瀏覽器緩存,CDN/代理不緩存(適合用戶專屬圖片、驗證碼)。
  • public:允許 CDN/代理緩存(適合公共靜態資源、水印圖)。

進階:Swoole/RoadRunner 長連接環境下的注意事項

  • 每次請求必須重新發送所有 Header,框架可能默認複用 Response 對象,需手動 $response->clearHeaders()。
  • ob_get_length() 在無 OB 時返回 false,Content-Length 可選(Chunked Transfer 可自動處理)。
  • ETag 生成邏輯避免每次 md5_file() 讀磁盤:用 md5($path . filemtime($path) . filesize($path)) 或版本號。

常見錯誤避雷

錯誤後果修正
Header 後有空白/換行輸出Cannot modify header information確保 header() 前無任何 echo/HTML/BOM
用 date() 而非 gmdate() 生成 GMT 時間時區偏移導致緩存判斷錯誤統一 gmdate('D, d M Y H:i:s') . ' GMT'
ETag 未加雙引號不符合 RFC 7232,瀏覽器可能忽略header("ETag: \"$hash\"")
同時發送 no-cache 和 max-age指令衝突,行為未定義按需二選一

總結

PHP 輸出圖片的完整緩存策略 = 強緩存 + 協商緩存雙層保障。代碼模板已處理:

  1. Cache-Control + Expires 控制強緩存期
  2. Last-Modified + ETag 提供協商緩存標識
  3. 優先檢查 If-None-Match 再檢查 If-Modified-Since
  4. 命中返回 304 即刻退出,節省圖片處理 CPU

將此模板封裝為 ImageResponse::send($path, $maxAge) 靜態方法,即可在全專案複用,顯著降低圖片類接口的平均響應時間與帶寬成本。

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