檔案上傳是 Web 開發常見需求,但隨著業務複雜度提升,單純的表單提交已難以滿足即時驗證、大檔分片、重複上傳等場景。本文歸納三大典型業務場景,並給出「先上傳暫存、最後提交時移動」的核心優化思路,附完整前後端代碼示範。
最基礎的情況:使用者選擇單一檔案,隨表單一併提交。後端直接處理 $_FILES 即可,無額外優化空間。
使用者選檔後需立即上傳解析(如 APK 包名、版本號),並在頁面顯示資訊;最終提交時若再傳一次檔案,會浪費頻寬與時間。
優化策略:首次上傳後將檔案存為伺服器暫存檔,並將暫存路徑等元資料寫入隱藏欄位;最終提交時只傳遞元資料,後端直接移動暫存檔至正式目錄。
例如 100MB 檔案分 10 次上傳,每次 10MB。單次請求不超限,但合併後會超過 upload_max_filesize。同樣採用暫存策略:每片上傳後暫存,最後合併或僅記錄元資料。
$('.upload-real-file').off().on('change', function(){
if(!$(this).val()) return false;
var responseObjId = $(this).attr('response-id');
var responseObj = $('#' + responseObjId);
$('#Form').ajaxSubmit({
url: '/aa/bb/uploadTmpApkTool',
resetForm: false,
dataType: 'json',
beforeSubmit: function(){ window.loading = layer.load(2); },
success: function(data){
layer.close(window.loading);
if(data.status == 1){
responseObj.html(data.apkInfoHtml);
var parentContainer = responseObj.parent().parent(),
nameContainer = parentContainer.find('.file-name-container');
nameContainer.html(data.apkName).attr('title', data.apkName);
responseObj.find('.file-tmp').html(data.fileInfo); // 隱藏域存暫存資訊
$(submitId).removeAttr('disabled');
}else{
layer.alert(data.info);
}
},
error: function(){
layer.close(window.loading);
layer.alert('未知錯誤,請稍後再試!');
}
});
return false; // 防止 dialog 自動關閉
});
$apkConfig = $this->_getApkConfig();
$subFile = $_FILES['apkToolFiles'];
$apkName = $apkInfoHtml = "";
if(empty($subFile)) {
$this->ajaxReturn(['status' => -4, 'info' => '請選擇要上傳的文件']);
}
foreach ($subFile['name'] as $subKey => $subVal) {
if ($subFile['name'][$subKey]) {
$fileData = $this->_getFileData($subFile, $subKey);
$checkData = [
'maxSize' => $apkConfig['FILE_MAX_SIZE'],
'savePath' => $apkConfig['TMP_CHILD_PATH'],
'extArr' => ['apk'],
'releaseName' => str_replace('.apk', '', $fileData['fileName']),
];
$checkResult = $this->_checkFileData($fileData, $checkData);
if ($checkResult['status'] != 1) { $this->ajaxReturn($checkResult); }
// 移動到暫存目錄,檔名加隨機後綴防衝突
$filePath = $checkData['savePath'] . '/' . $fileData['fileName'] . '.tmp' . genRandStr(6);
$this->_moveUploadedFile($fileData['tmpName'], $filePath);
// 解析 APK 資訊
$apkInfo = $this->_apkParser($filePath);
if($apkInfo['UMENG_CHANNEL'] != 'UMENG_CHANNEL_VALUE') {
@unlink($filePath);
$this->ajaxReturn(['status' => 0, 'info' => "UMENG_CHANNEL 的值必須為 UMENG_CHANNEL_VALUE"]);
}
// 組裝暫存檔資訊供前端隱藏域使用
$tmpFileArr['file_info'] = [
'name' => $subFile['name'][$subKey],
'type' => $subFile['type'][$subKey],
'tmp_name' => str_replace($apkConfig['TMP_CHILD_PATH'] . '/', '', $filePath),
'error' => $subFile['error'][$subKey],
'size' => $subFile['size'][$subKey],
];
} else {
$this->ajaxReturn(['status' => 0, 'info' => "文件不能為空"]);
}
foreach ($apkInfo as $key => $val) {
$apkInfoHtml .= "{$key}:{$val} \r\n";
}
$apkName = $fileData['fileName'];
$version = $apkInfo['versionName'];
}
$fileInfo = htmlspecialchars(json_encode($tmpFileArr['file_info']));
$fileInfoHtml = "<input name=\"apkToolFileTmp[]\" value='{$fileInfo}' type=\"hidden\"/>";
$this->ajaxReturn([
'status' => 1, 'info' => "上傳成功",
'version' => $version, 'apkName' => $apkName,
'apkInfoHtml' => $apkInfoHtml, 'fileInfo' => $fileInfoHtml
]);
// 提交表單前禁用實體 file input,避免重複上傳
$('.upload-file-real').attr('disabled', 'disabled');
後端接收隱藏域 apkToolFileTmp[],解析 JSON 取得暫存路徑,直接 rename() 至正式目錄即可。
使用者可能中途取消操作,導致暫存檔殘留。建議寫一個 Cron 腳本,每日掃描暫存目錄,刪除建立時間超過 24 小時的檔案,並記錄日誌。
「先暫存、後移動」模式有效解決重複上傳、大檔分片、即時驗證三大痛點。關鍵在於:前端上傳後拿到暫存資訊寫入隱藏域、後端驗證通過才落盤、正式提交時僅移動檔案、定時清理孤兒檔。掌握此模式,檔案上傳功能即能優雅應對複雜業務。