如何实现表单提交前预上传大文件?求技术方案与代码指引
问题描述
我现有一套带文件名转换、文件大小校验、文件扩展名校验的文件上传表单,上传500MB这类大文件时,网络环境良好能正常完成,但移动WiFi这类慢网络下会因请求超时被取消。我想实现:
- 文件附加时自动预上传
- 显示上传进度条和取消按钮
- 预上传完成前禁止提交按钮
- 最终提交时仅把临时文件夹里的文件移到上传目录
以下是当前代码和配置:
当前上传表单代码
<button type="submit" class="btn btn-primary btn-xs" id="add_file" name="add_file" form="upload-form" style="display:none;">Subir archivos</button> <form action="info_order.php?order=<?php echo $order; ?>" id="upload-form" method="post" enctype="multipart/form-data"> <?php if((strpos($_SERVER['HTTP_USER_AGENT'],'Windows'))||(strpos($_SERVER['HTTP_USER_AGENT'],'Macintosh'))){ ?> <div id="dropzone" class="drop-zone" style="display:none;"> <span class="drop-zone__prompt">Haga click o arrastre los archivos que desea subir.</span> <input type="file" name="file-upload[]" style="display:none;" id="file-upload" class="drop-zone__input" multiple> </div> <?php } elseif((strpos($_SERVER['HTTP_USER_AGENT'],'Android'))||(strpos($_SERVER['HTTP_USER_AGENT'],'iPhone'))){ ?> <input type="file" name="file-upload[]" id="file-upload2" style="display:none;" onchange="document.getElementById('add_file').click()" multiple> <button type="button" class="btn btn-primary" onclick="document.getElementById('file-upload2').click()">Subir archivos</button> </input> <?php } ?> </form>
POST提交代码
if(isset($_POST['add_file'])) { $error_message = ''; $ok = 0; $nook = 0; $new_value = ''; $array_ext = array('pdf','docx','doc','xlsx','xls','xlsb','jpg','jpeg','png','mp3','mp4','msg','txt','zw1','7z','zip','psd','skr'); if($_POST['admin-upload'] == true){ $access_level = 1; } else { $access_level = 2; } $total_count = count($_FILES['file-upload']['name']); for($i=0;$i<$total_count;$i++) { $filename = $_FILES['file-upload']['name'][$i]; $tmpFilePath = $_FILES['file-upload']['tmp_name'][$i]; $filename = str_replace("'","",strtolower($filename)); $filename = $order.'_'.$filename; if($filename != ''){ $ext = strtolower(substr( $filename, strrpos( $filename, '.' ) + 1 ) ); if(in_array($ext, $array_ext)) { $upload_path = 'uploads/orders'; if(is_writable($tmpFilePath)){ $search_file = $upload_path.'/'.$filename; if(file_exists($search_file)){ for($i=2;$i<999;$i++) { $filename_noext = basename($filename,'.'.$ext); $new_search_file = $upload_path.'/'.$filename_noext.'('.$i.').'.$ext; if(!file_exists($new_search_file)){ $filename = $filename_noext.'('.$i.').'.$ext; break; } } $search_file = $new_search_file; } if(($info_order['Archivos adjuntos'] == "")&&($new_value == '')){ $new_value = $filename.'β'.$access_level; } elseif(($info_order['Archivos adjuntos'] != "")&&($new_value == '')){ $new_value = $info_order['Archivos adjuntos'].'α'.$filename.'β'.$access_level; } else { $new_value .= 'α'.$filename.'β'.$access_level; } $update_date = strftime("%Y-%m-%d %H:%M:%S"); $tracking_date = strftime("%d/%m/%Y - %H:%M"); $newtrack = '#'."<b>".$tracking_date."</b><br/>El usuario ".$name_user." ha subido el fichero ".$filename."."; $tracking .= $newtrack; $sql = "UPDATE `ordenes` SET `Archivos adjuntos` = '{$new_value}', `Fecha última modificación` = '{$update_date}', `Tracking` = '{$tracking}' WHERE `Nº orden` = '{$order}'"; if($db->query($sql)){ if(move_uploaded_file($tmpFilePath,$search_file)){ unset($tmpFilePath); $error_message .= '<br/><li>El fichero '.$filename.' se ha subido correctamente.</li>'; $ok++; } else { $error_message .= '<br/><li>El fichero '.$filename.' no se pudo subir al directorio correctamente.</li>'; $nook++; } } else { $error_message .= '<br/><li>El fichero '.$filename.' no se pudo agregar a la base de datos correctamente.</li>'; $nook++; } } else { $error_message .= '<br/><li>El fichero no se puede subir porque no tiene permisos de escritura.</li>'; $nook++; } } else { $error_message .= '<br/><li>El fichero '.$filename.' no se puede subir porque la extensión no está permitida.</li>'; $nook++; } } else { $error_message .= '<br/><li>El fichero no se puede subir porque no tiene nombre.</li>'; $nook++; } } if($nook == 0){ $session->msg('s',$error_message); } elseif($ok == 0){ $session->msg('d',$error_message); } else { $session->msg('w',$error_message); } redirect('info_order.php?order='.$order); }
php.ini配置

解决方案
技术栈选择
- 前端:原生JavaScript处理文件上传、进度监听、取消请求;HTML/CSS实现进度条和按钮状态控制
- 后端:PHP(沿用现有技术栈),新增预上传接口、文件移动接口,负责临时文件管理
前端改造要点
- 新增进度条与取消按钮
在表单中添加进度展示区域:
<!-- 进度条容器 --> <div id="upload-progress" style="display:none; margin:10px 0;"> <div class="progress-bar" style="width:0%; height:20px; background:#007bff; transition:width 0.3s;"></div> <span id="progress-text">0%</span> <button type="button" id="cancel-upload" class="btn btn-danger btn-xs" style="margin-left:10px;">取消上传</button> </div> <!-- 提交按钮(初始禁用) --> <button type="submit" class="btn btn-primary btn-xs" id="submit-btn" name="submit-btn" form="upload-form" disabled>确认提交</button>
- 文件选择后自动预上传
监听文件选择事件,用XMLHttpRequest发送文件到预上传接口,实时监听上传进度:
let xhr; // 绑定桌面端文件选择事件 document.getElementById('file-upload').addEventListener('change', handleFileSelect); // 绑定移动端文件选择事件 document.getElementById('file-upload2').addEventListener('change', handleFileSelect); function handleFileSelect(e) { const files = e.target.files; if (files.length === 0) return; // 显示进度条,禁用提交按钮 document.getElementById('upload-progress').style.display = 'block'; document.getElementById('submit-btn').disabled = true; // 构建FormData const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('pre-upload[]', files[i]); // 传递订单号用于临时文件分类 formData.append('order', '<?php echo $order; ?>'); } // 初始化XHR请求 xhr = new XMLHttpRequest(); xhr.open('POST', 'pre_upload.php', true); // 监听上传进度 xhr.upload.addEventListener('progress', function(e) { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); document.querySelector('.progress-bar').style.width = percent + '%'; document.getElementById('progress-text').textContent = percent + '%'; } }); // 上传完成回调 xhr.addEventListener('load', function() { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); if (response.success) { // 保存临时文件标识到表单,提交时使用 const hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = 'temp_files'; hiddenInput.value = JSON.stringify(response.temp_files); document.getElementById('upload-form').appendChild(hiddenInput); // 启用提交按钮,隐藏进度条 document.getElementById('submit-btn').disabled = false; document.getElementById('upload-progress').style.display = 'none'; } else { alert('预上传失败:' + response.message); } } else { alert('预上传请求失败'); } }); // 绑定取消上传事件 document.getElementById('cancel-upload').addEventListener('click', function() { if (xhr) xhr.abort(); document.getElementById('upload-progress').style.display = 'none'; document.getElementById('submit-btn').disabled = false; // 清空文件选择框 e.target.value = ''; }); // 发送预上传请求 xhr.send(formData); }
后端改造要点
- 预上传接口(pre_upload.php)
负责接收文件并保存到订单专属临时目录,返回临时文件信息:
<?php session_start(); $order = $_POST['order']; $temp_dir = 'uploads/temp/' . $order . '/'; // 创建订单专属临时目录 if (!is_dir($temp_dir)) { mkdir($temp_dir, 0755, true); } $allowed_ext = array('pdf','docx','doc','xlsx','xls','xlsb','jpg','jpeg','png','mp3','mp4','msg','txt','zw1','7z','zip','psd','skr'); $temp_files = []; foreach ($_FILES['pre-upload']['name'] as $key => $filename) { $tmp_path = $_FILES['pre-upload']['tmp_name'][$key]; $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); // 扩展名校验 if (!in_array($ext, $allowed_ext)) { echo json_encode(['success' => false, 'message' => '文件扩展名不允许']); exit; } // 文件名处理(和原有逻辑一致) $clean_filename = str_replace("'","",strtolower($filename)); $clean_filename = $order . '_' . $clean_filename; $temp_file_path = $temp_dir . $clean_filename; // 重名处理 $counter = 2; while (file_exists($temp_file_path)) { $filename_noext = pathinfo($clean_filename, PATHINFO_FILENAME); $temp_file_path = $temp_dir . $filename_noext . '(' . $counter . ').' . $ext; $counter++; } // 保存到临时目录 if (move_uploaded_file($tmp_path, $temp_file_path)) { $temp_files[] = [ 'temp_path' => $temp_file_path, 'final_filename' => basename($temp_file_path) ]; } else { echo json_encode(['success' => false, 'message' => '文件保存失败']); exit; } } echo json_encode(['success' => true, 'temp_files' => $temp_files]); ?>
- 修改原提交接口(info_order.php)
不再处理文件上传,改为读取临时文件路径并移动到正式上传目录:
if(isset($_POST['submit-btn'])) { $error_message = ''; $ok = 0; $nook = 0; $new_value = ''; $array_ext = array('pdf','docx','doc','xlsx','xls','xlsb','jpg','jpeg','png','mp3','mp4','msg','txt','zw1','7z','zip','psd','skr'); $access_level = isset($_POST['admin-upload']) && $_POST['admin-upload'] == true ? 1 : 2; $temp_files = json_decode($_POST['temp_files'], true); $upload_path = 'uploads/orders'; foreach ($temp_files as $file) { $temp_path = $file['temp_path']; $final_filename = $file['final_filename']; $ext = strtolower(pathinfo($final_filename, PATHINFO_EXTENSION)); // 二次校验扩展名(安全加固) if(!in_array($ext, $array_ext)) { $error_message .= '<br/><li>文件'.$final_filename.'扩展名不允许</li>'; $nook++; continue; } $final_path = $upload_path . '/' . $final_filename; // 移动文件到正式目录 if(rename($temp_path, $final_path)) { // 数据库更新逻辑(沿用原有逻辑) if(($info_order['Archivos adjuntos'] == "")&&($new_value == '')){ $new_value = $final_filename.'β'.$access_level; } elseif(($info_order['Archivos adjuntos'] != "")&&($new_value == '')){ $new_value = $info_order['Archivos adjuntos'].'α'.$final_filename.'β'.$access_level; } else { $new_value .= 'α'.$final_filename.'β'.$access_level; } $update_date = strftime("%Y-%m-%d %H:%M:%S"); $tracking_date = strftime("%d/%m/%Y - %H:%M"); $newtrack = '#'."<b>".$tracking_date."</b><br/>El usuario ".$name_user." ha subido el fichero ".$final_filename."."; $tracking .= $newtrack; $sql = "UPDATE `ordenes` SET `Archivos adjuntos` = '{$new_value}', `Fecha última modificación` = '{$update_date}', `Tracking` = '{$tracking}' WHERE `Nº orden` = '{$order}'"; if($db->query($sql)){ $error_message .= '<br/><li>文件'.$final_filename.'上传成功</li>'; $ok++; } else { $error_message .= '<br/><li>文件'.$final_filename.'数据库更新失败</li>'; $nook++; } // 删除空临时目录 $temp_dir = dirname($temp_path); if(count(scandir($temp_dir)) == 2) { rmdir($temp_dir); } } else { $error_message .= '<br/><li>文件'.$final_filename.'移动失败</li>'; $nook++; } } if($nook == 0){ $session->msg('s',$error_message); } elseif($ok == 0){ $session->msg('d',$error_message); } else { $session->msg('w',$error_message); } redirect('info_order.php?order='.$order); }
关键注意事项
- php.ini配置调整:将
upload_max_filesize、post_max_size设为1G以上,max_execution_time和max_input_time调至300秒以上,避免预上传超时 - 临时文件清理:添加定时任务(如Linux Cron)定期清理超过24小时的临时文件
- 中断处理:后端可检测XHR中断事件,删除已上传的临时文件片段
- 移动端适配:保留原有移动端文件选择逻辑,替换自动提交为预上传逻辑
内容的提问来源于stack exchange,提问作者leesin koreano
相关产品推荐
相关产品推荐

