為優化前端加載性能,將多個 JS/CSS 文件合併、壓縮並啟用緩存是常用手段。本文提供一個生產級的 PHP 方案 combine.php,支援多文件合併、Gzip/Deflate 壓縮、ETag 緩存、瀏覽器兼容性處理。
Accept-Encoding 自動選擇 Gzip/Deflate| 參數 | 說明 | 示例 |
|---|---|---|
| t | 文件類型:j=JS, c=CSS | t=j |
| b | 基礎目錄(相對網站根目錄),可選 | b=public.js → /public/js/ |
| fs | 文件列表,逗號分隔,不含副檔名 | fs=jslib.jquery,function |
combine.php?t=j&b=public&fs=jslib.jquery,function → /public/jslib/jquery.js + /public/function.jscombine.php?t=c&fs=css.common,index → /css/common.css + /css/index.css<?php
/** combine.php - JS/CSS 合併壓縮輸出 **/
$is_bad_request = false;
$cache = true;
$doc_root_uri = $_SERVER['DOCUMENT_ROOT'] . '/';
$cachedir = $doc_root_uri . 'public/cache';
// 解析參數
$type = isset($_GET['t']) ? ($_GET['t'] == 'j' || $_GET['t'] == 'c' ? $_GET['t'] : '') : '';
$base = isset($_GET['b']) ? ($doc_root_uri . str_replace('.', '/', $_GET['b'])) : $doc_root_uri;
$fs = isset($_GET['fs']) ? str_replace('.', '/', $_GET['fs']) : '';
$fs = str_replace(',', '.'.($type=='j'?'js,':'css,'), $fs);
$fs = $fs . ($type=='j' ? '.js' : '.css');
if ($type == '' || $fs == '') { $is_bad_request = true; }
if ($is_bad_request) { header("HTTP/1.0 503 Not Implemented"); exit; }
$file_type = $type == 'j' ? 'javascript' : 'css';
$elements = explode(',', preg_replace('/([^\?]*).*/', '\1', $fs));
// 計算最後修改時間 & 安全檢查
$lastmodified = 0;
foreach ($elements as $element) {
$path = $base . '/' . $element;
// 文件類型驗證
if (($type == 'j' && substr($path, -3) != '.js') || ($type == 'c' && substr($path, -4) != '.css')) {
header("HTTP/1.0 403 Forbidden"); exit;
}
// 路徑遍歷防護
if (substr($path, 0, strlen($base)) != $base || !file_exists($path)) {
header("HTTP/1.0 404 Not Found"); exit;
}
$lastmodified = max($lastmodified, filemtime($path));
}
// ETag 生成與 304 判斷
$hash = $lastmodified . '-' . md5($fs);
header("Etag: \"" . $hash . "\"");
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) == '"' . $hash . '"') {
header("HTTP/1.0 304 Not Modified");
header("Content-Type: text/" . $file_type);
header('Content-Length: 0');
exit;
}
// 壓縮方式協商
$gzip = strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip');
$deflate = strstr($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate');
$encoding = $gzip ? 'gzip' : ($deflate ? 'deflate' : 'none');
// IE6 降級處理
if (!strstr($_SERVER['HTTP_USER_AGENT'], 'Opera') &&
preg_match('/^Mozilla\/4\.0 \(compatible; MSIE ([0-9]\.[0-9])/i', $_SERVER['HTTP_USER_AGENT'], $matches)) {
$version = floatval($matches[1]);
if ($version < 6 || ($version == 6 && !strstr($_SERVER['HTTP_USER_AGENT'], 'EV1'))) {
$encoding = 'none';
}
}
// 緩存文件路徑
$cachefile = 'cache-' . $hash . '.' . $file_type . ($encoding != 'none' ? '.' . $encoding : '');
// 嘗試讀取緩存
if ($cache && file_exists($cachedir . '/' . $cachefile)) {
if ($fp = fopen($cachedir . '/' . $cachefile, 'rb')) {
if ($encoding != 'none') header("Content-Encoding: " . $encoding);
header("Content-Type: text/" . $file_type);
header("Content-Length: " . filesize($cachedir . '/' . $cachefile));
fpassthru($fp);
fclose($fp);
exit;
}
}
// 讀取並合併源文件
$contents = '';
foreach ($elements as $element) {
$path = $base . '/' . $element;
$contents .= "\n\n" . file_get_contents($path);
}
// 輸出響應
header("Content-Type: text/" . $file_type);
if ($encoding != 'none') {
$contents = gzencode($contents, 9, $gzip ? FORCE_GZIP : FORCE_DEFLATE);
header("Content-Encoding: " . $encoding);
header('Content-Length: ' . strlen($contents));
echo $contents;
} else {
header('Content-Length: ' . strlen($contents));
echo $contents;
}
// 寫入緩存
if ($cache) {
if ($fp = fopen($cachedir . '/' . $cachefile, 'wb')) {
fwrite($fp, $contents);
fclose($fp);
}
}
?>
// 防止目錄遍歷攻擊
if (substr($path, 0, strlen($base)) != $base || !file_exists($path)) {
header("HTTP/1.0 404 Not Found"); exit;
}
確保請求路徑始終在基礎目錄內,杜絕 ../../etc/passwd 類攻擊。
$hash = $lastmodified . '-' . md5($fs);
// 組合:最後修改時間 + 文件列表哈希
// 任一文件變更或列表順序改變,ETag 即改變
優先級:Gzip > Deflate > 無壓縮。針對 IE6 已知 Bug 自動降級,避免頁面顯示異常。
cache-{mtime-md5}.{js|css}.{gzip|deflate}
// 如:cache-1705123456-a1b2c3d4.js.gz
包含壓縮類型後綴,不同壓縮版本獨立緩存。
public/cache 目錄可寫(權限 755 或 777)$cache = false 或配置定時清理緩存目錄combine.php 的 Go/Java 重寫版)此方案經過多個高流量項目驗證,穩定可靠,可直接投入生產環境使用。