PHP大文件分片上传与进度条实现:切分、并发、合并全攻略
发布时间:2026/10/17 20:41:36 锦皓数字建站

1. 为什么大文件上传要先解决进度条失灵的问题做PHP开发的朋友迟早会遇到这样一个需求用户要上传一个几个G的视频素材或者一份几百MB的设计源文件后台明明用的是再普通不过的form表单加input typefile前端也写了ajax上传但真正把文件拖进去一点上传就会发现两个尴尬问题。第一个问题是服务器直接报错post_max_size和upload_max_filesize双双超限nginx的client_max_body_size也来凑热闹文件压根没到PHP就被拦截了。第二个问题是就算把所有限制都调大上传能跑起来进度条却像死了一样——卡在0%半天不动然后突然跳到100%或者干脆一直转圈。这个现象在超过1GB的文件上尤其明显因为网络抖动、服务器超时、代理超时任何一环断开整个请求就废了而浏览器还傻傻地在那里等。我在实际项目中第一次被这个问题打疼是某跨平台系统里要支持用户上传4K短视频素材。单个文件动辄2GB测试环境用的还是本地上传看起来一切正常一放到真实网络环境就原形毕露一个2GB的文件单个HTTP请求持续两三分钟中间任何一秒网络波动进度条就永远停在某个百分比再过一会儿整个请求超时报错用户重新上传又得从头再来。这里我先把结论摆出来大文件上传必须做分片进度条失灵的根源在于你把整个文件塞进了一个难以控制的单一请求里。分片把一个大请求拆成多个小请求每个小请求都能被浏览器准确测量进度还能独立重试进度条才能做到真实、平滑、可持续。1.1 单请求上传的三大天花板先别急着写代码得先把为什么单个请求搞不定大文件这件事想透。这里有三道天花板每一道都能让你的上传方案废掉。第一道是服务器配置限制。PHP默认post_max_size只有8MBupload_max_filesize也是2MB你没看错默认就是这么小。就算你改了这两项web服务器层还有限制——nginx默认client_max_body_size是1MBApache的LimitRequestBody默认是0表示不限制但实际也受其他因素影响。曾经有同事把PHP配置调对了忘了改nginx排查了一下午。第二道是请求超时与网络波动。一个2GB的文件假设上行带宽是5MB/s总共需要400秒。php的max_execution_time默认30秒fastcgi的request_terminate_timeout也普遍设置为60~120秒。除非你把所有超时参数全部调大否则请求几乎必然中断。就算你把超时调到9999用户网络一波动TCP连接断了整个上传就得重来。第三道是服务器内存压力。PHP接收上传文件时文件先写入临时目录但在某些处理方式下比如直接用php://input读原流数据要整个在内存里往返或者经过move_uploaded_file之后的二次处理内存占用会瞬间飙高。多个用户同时传大文件服务器直接就OOM了。这三道天花板决定了单请求上传大文件是一条死路。分片的意义首先是绕开限制其次才是配合进度条体验优化。1.2 分片上传解决的不只是进度条分片上传的核心思路很简单前端把文件切成若干小块每个小块单独发一个HTTP请求后端把这些小块按顺序存下来全部传完后再合并成完整文件。这样一来每个请求只有几MB远低于各项配置限制请求时间也短单个分片失败只需要重传这个分片而不是整个文件。进度条反而只是分片上传带来的一个副产品——因为每个分片上传都是独立且可计量的总进度天然可以通过已完成分片数/全部分片数算出来。更关键的是分片之后才能做断点续传和秒传。这是后话后面专门展开。现在只需要建立一个认知分片是基础设施进度条是建在这个设施上的应用。2. 分片上传的核心架构前端切分、后端暂存、按序合并分片上传的架构图不需要画得多复杂核心就三件事前端把文件切成块、后端把块接收下来暂存、最后合并成完整文件。听起来简单但每一步都有细节坑。2.1 文件切分的粒度选择先说分片大小。我见过有人随手把每个分片设为1MB也见过有人设成50MB这两个极端在实际中都会出问题。分片太小比如1MB意味着一个2GB文件要切2048片每个分片都要发起一次HTTP请求。这些请求的握手、请求头、响应解析等固定开销会占据很大比例总耗时反而变长并发控制也更复杂。分片太大比如50MB又回到了单请求的老路虽然比2GB好一点但超时风险依然很高对服务器内存的压力也大。我的实践经验是分片大小按网络状况和文件大小动态取基准值4~10MB比较稳。内网环境可以放大到20MB公网环境在5MB左右比较合适。判断标准很简单一个分片的传输时间控制在1~3秒内这样单个分片失败后重试成本很低进度条更新频率也足够平滑。前端切片的代码其实只有一行核心逻辑就是Blob.prototype.slice。比如以下代码const file document.getElementById(fileInput).files[0]; const chunkSize 5 * 1024 * 1024; // 5MB const totalChunks Math.ceil(file.size / chunkSize); const chunkList []; for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const chunk file.slice(start, end); chunkList.push({ chunk, index: i }); }这里的chunk是一个Blob对象可以直接放进FormData里随请求上传。index字段用于后端排序后续合并时必须用它保证顺序。2.2 后端如何安全接收分片PHP后端接收分片我的标准做法是每个分片单独存成一个临时文件文件名包含一个全局唯一的uploadId和分片序号。uploadId通常是前端在当前会话里生成的一个UUID或者基于文件内容生成的唯一标识。目录结构大概是这样的upload_temp/ 20250609_ab12cd34ef/ 0.part 1.part 2.part ...这里的20250609_ab12cd34ef就是一个uploadId对应的目录。接收分片的PHP接口核心逻辑如下// upload_chunk.php $uploadId $_POST[upload_id] ?? ; $chunkIndex (int)($_POST[chunk_index] ?? 0); $totalChunks (int)($_POST[total_chunks] ?? 0); if ($uploadId || !isset($_FILES[chunk])) { http_response_code(400); echo json_encode([code 400, msg 参数不完整]); exit; } // 校验uploadId格式只允许字母数字下划线防止目录穿越 if (!preg_match(/^[a-zA-Z0-9_]$/, $uploadId)) { http_response_code(400); echo json_encode([code 400, msg 非法uploadId]); exit; } $uploadDir __DIR__ . /upload_temp/ . $uploadId; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0775, true); } $chunkFile $uploadDir . / . $chunkIndex . .part; // 注意move_uploaded_file比file_put_contents更安全因为它校验了是否是PHP接收到的临时文件 if (!move_uploaded_file($_FILES[chunk][tmp_name], $chunkFile)) { http_response_code(500); echo json_encode([code 500, msg 分片保存失败]); exit; } // 返回当前分片总进度供前端展示或校验 $uploadedChunks count(glob($uploadDir . /*.part)); echo json_encode([ code 0, uploaded_chunks $uploadedChunks, total_chunks $totalChunks ]);有几个细节必须注意。$_FILES[chunk][error]一定要检查因为不同错误码代表不同原因比如UPLOAD_ERR_INI_SIZE说明这个分片超过了post_max_size如果分片是5MB而post_max_size是8MB理论上没问题但如果有多个分片并发上传表单数据可能超过限制所以post_max_size要适当放大。另外uploadId的合法性校验不能省。如果直接用前端传来的字符串拼路径恶意请求可以传../../etc/之类的值搞目录穿越。这个校验在安全上是一条底线。2.3 合并策略与文件校验全部分片传完之后需要一个合并接口把所有.part文件按顺序拼成一个完整的文件。PHP端最简单的做法是逐个读取分片写入目标文件// merge.php $uploadId $_POST[upload_id] ?? ; $totalChunks (int)($_POST[total_chunks] ?? 0); $originalName $_POST[original_name] ?? upload.bin; $uploadDir __DIR__ . /upload_temp/ . $uploadId; // 检查分片是否齐全 $allChunks glob($uploadDir . /*.part); if (count($allChunks) ! $totalChunks) { http_response_code(400); echo json_encode([code 400, msg 分片不完整无法合并]); exit; } $destFile __DIR__ . /uploads/ . $uploadId . _ . basename($originalName); $fp fopen($destFile, wb); for ($i 0; $i $totalChunks; $i) { $partFile $uploadDir . / . $i . .part; $part file_get_contents($partFile); fwrite($fp, $part); unlink($partFile); // 合并后删除分片释放空间 } fclose($fp); echo json_encode([code 0, msg 合并成功, path $destFile]);这段代码能跑但有两个问题需要优化。一是file_get_contents会把整个分片读进内存如果分片是5MB那还行如果调大了分片或者并发合并任务多了内存照样吃紧。更稳妥的方式是用fopen配合stream_copy_to_stream或者fread循环写入。二是没有校验分片完整性。每个分片上传时前端可以计算分片的MD5后端保存分片时同时记录MD5值合并前把所有分片的MD5校验一遍能有效避免传完了但文件损坏的情况。对于超大文件逐分片MD5的计算本身也有成本所以通常只在关键场景做比如金融、医疗类系统的文件上传或者对文件完整性要求极高的素材交付场景。3. 进度条实现的两种路线本地XHR进度与后端服务端进度分片上传跑通之后进度条方案就有的选了。根据我的经验实现进度条有两条路线很多人一开始会混淆。3.1 方案一依赖XMLHttpRequest的实时上传进度浏览器里的XMLHttpRequest对象有一个upload属性和一个progress事件可以直接拿到当前请求上传了多少字节。这个事件是浏览器网络层实时触发的数据很真实。对于单请求上传进度条终极写法就是监听这个事件xhr.upload.addEventListener(progress, (e) { if (e.lengthComputable) { const percent Math.round((e.loaded / e.total) * 100); updateProgressBar(percent); } });但是在分片上传场景下这个方案需要做一些变通。因为每个分片都是一个独立的XHR请求每个请求都有自己的e.loaded和e.total。如果并发3个分片同时上传你能拿到的是这3个分片各自的实时进度而整个文件的总进度需要你自己算。算法不复杂总进度 已完成分片数 当前正在上传的分片实时进度权重。用公式表示就是文件总进度 (已上传完的分片数 Σ(每个在途分片的e.loaded / e.total)) / 总分片数这里有个细节在途分片的进度如果叠加进度条会呈现锯齿状增长这个后面在实操部分会给出平滑方案。3.2 方案二统一异步任务进度用轮询接口上报如果项目里的分片上传是交给一个异步队列处理或者你不希望前端XHR事件来驱动进度那可以走另一条路后端维护上传状态的数据库记录前端定时轮询状态接口拿到进度百分比。这种方案在以下场景更适用服务端接收分片后不是立刻落盘而是先存入消息队列由worker进程异步处理。前端和后端是分离部署的开发进度条的前端工程师不方便接触上传逻辑。需要同时向多个用户展示上传进度比如一个协作文档系统里A上传素材B能看到上传进度。具体做法是分片上传接口每次收到一个分片时更新Redis或者数据库里对应uploadId的uploaded_chunks字段。前端开启一个setInterval或者requestAnimationFrame循环每500ms到1s请求一次upload_status.php?upload_idxxx拿到{ uploaded_chunks, total_chunks, status }后刷新进度条。3.3 两种方案怎么选并发分片多了以后XHR进度并不能直接代表文件进度很多初次接触分片上传的开发者会想当然地认为每个分片都有XHR进度加起来不就是总进度吗。这个思路对但实现上有大坑。当你的并发数只有1、2个的时候直接累加在途分片的实时进度进度条看起来很顺滑。但当你把并发数提升到4~6个之后会出现一个问题XHR的progress事件触发频率非常高高到你的回调函数里如果做了DOM操作浏览器会卡顿进度条像疯了一样跳动。另外如果你对在途分片的进度做了乐观预估比如认为它匀速增长而实际某个分片卡在了99%迟迟不发完你的总进度就会长时间停在某个数值不动——视觉上反倒是准确的。我现在的判断标准是如果并发数在1~3个用XHR实时进度如果并发数超过3个或者需要断点续传的场景统一用已完成分片数/总分片数作为主要进度指标分片内部的XHR进度仅用于单个分片的细节展示。前面那种累加方式听起来精确但在工程上很难做好看反而简单的已完成/总数最可靠因为每个分片就几MB传输时间1~3秒进度条每秒跳一次其实已经够用了。4. 实战落地一个可运行的分片上传进度条最小实现理论说再多不如直接上一份能跑起来的代码。这里我给出一个最小实现前端用原生JavaScript后端用PHP不依赖任何框架方便你移植到自己项目里。4.1 前端代码切片、并发上传、进度汇总计算先看HTML结构一个文件选择框、一个进度条、一个状态文本input typefile idfileInput / div classprogress-container div classprogress-bar idprogressBar stylewidth: 0%;/div /div span idprogressText0%/spanJavaScript核心逻辑分四步选择文件后初始化上传会话把文件切成chunkSize大小的分片以固定并发数上传分片全部完成后调用合并接口。const fileInput document.getElementById(fileInput); const progressBar document.getElementById(progressBar); const progressText document.getElementById(progressText); // 生成上传会话ID function generateUploadId() { return up_ Date.now() _ Math.random().toString(36).substr(2, 9); } fileInput.addEventListener(change, async (event) { const file event.target.files[0]; if (!file) return; const chunkSize 5 * 1024 * 1024; // 5MB const totalChunks Math.ceil(file.size / chunkSize); const uploadId generateUploadId(); const concurrency 3; // 并发数 // 生成分片任务列表 const tasks []; for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const chunk file.slice(start, end); tasks.push({ chunk, index: i }); } let finishedChunks 0; // 单个分片上传函数 function uploadChunk(task) { return new Promise((resolve, reject) { const formData new FormData(); formData.append(upload_id, uploadId); formData.append(chunk_index, task.index); formData.append(total_chunks, totalChunks); formData.append(chunk, task.chunk, chunk_${task.index}.part); const xhr new XMLHttpRequest(); xhr.open(POST, /upload_chunk.php); xhr.upload.addEventListener(progress, (e) { if (!e.lengthComputable) return; // 这里不直接更新总进度只用来打印单个分片的状态 }); xhr.onload () { if (xhr.status 200) { resolve(); } else { reject(new Error(分片${task.index}上传失败)); } }; xhr.onerror () reject(new Error(分片${task.index}网络错误)); xhr.send(formData); }); } // 并发控制维护一个任务队列始终有 concurrency 个请求在跑 async function runTasks() { const queue [...tasks]; const workers Array.from({ length: concurrency }, async () { while (queue.length 0) { const task queue.shift(); try { await uploadChunk(task); finishedChunks; updateProgress(); } catch (err) { // 简单重试一次 try { await uploadChunk(task); finishedChunks; updateProgress(); } catch (retryErr) { console.error(分片${task.index}两次失败停止队列); throw retryErr; } } } }); await Promise.all(workers); } function updateProgress() { const percent Math.round((finishedChunks / totalChunks) * 100); progressBar.style.width percent %; progressText.textContent percent %; } updateProgress(); await runTasks(); // 全部上传完成请求合并 const mergeForm new FormData(); mergeForm.append(upload_id, uploadId); mergeForm.append(total_chunks, totalChunks); mergeForm.append(original_name, file.name); await fetch(/merge.php, { method: POST, body: mergeForm }); progressBar.style.width 100%; progressText.textContent 100% - 上传完成; });这段代码里runTasks实现了一个简单的并发控制保持3个worker从队列里取任务每个worker上传完一个分片就取下一个直到队列清空。并发数从1改到6都行但建议不要超过6理由后面踩坑部分会说。4.2 后端代码接收分片、状态记录、合并接口后端PHP代码我在2.2节给过接收分片的核心版本。这里补充状态记录的实现让进度条不只是前端自嗨而是后端可查询的。我用一个简单文本文件来记录上传状态生产环境建议换成Redis文件内容是一行JSON// status_helper.php function getUploadStatus($uploadId) { $statusFile __DIR__ . /upload_temp/ . $uploadId . .status.json; if (file_exists($statusFile)) { return json_decode(file_get_contents($statusFile), true); } return [upload_id $uploadId, uploaded_chunks 0, total_chunks 0, status pending]; } function updateUploadStatus($uploadId, $uploadedChunks, $totalChunks, $status uploading) { $statusFile __DIR__ . /upload_temp/ . $uploadId . .status.json; $data [ upload_id $uploadId, uploaded_chunks $uploadedChunks, total_chunks $totalChunks, status $status ]; file_put_contents($statusFile, json_encode($data)); }然后在upload_chunk.php里每次成功保存一个分片后调用updateUploadStatus。再写一个查询接口// upload_status.php $uploadId $_GET[upload_id] ?? ; if (!preg_match(/^[a-zA-Z0-9_]$/, $uploadId)) { http_response_code(400); echo json_encode([code 400, msg 非法uploadId]); exit; } $status getUploadStatus($uploadId); echo json_encode([code 0, data $status]);这时候前端如果想轮询进度可以在setupPolling(uploadId)里每800ms请求一次这个接口刷新进度条。4.3 进度条UI的平滑处理技巧进度条这玩意儿做出来很容易做得让用户舒服则不容易。真实用户对进度条的耐心是有限的尤其是大文件上传1%到99%可能要等好几分钟如果进度条全程纹丝不动用户会觉得系统卡死了。我的经验有三个处理技巧。第一个技巧叫针对已完成分片的硬进度用并发中的软进度润滑。纯粹用已完成分片数/总分片数算出的进度是阶梯式的5MB一个分片传完一个跳一格。如果分片大小是10MB一个500MB的文件就50格进度条每6~10秒跳一次体感还行。但如果分片是50MB进度条几分钟才动一次用户会焦虑。解决办法是在累加时给每个在途分片加上它自己的XHR进度乘以一个权重让总进度在两个分片完成之间的间隙也能平滑增长。我在前面的公式里已经写过这里提醒一下计算公式没问题但要注意给在途分片进度加个权重上限防止它把总进度拉过头导致总进度到90%甚至95%了但实际上还有好几个分片没开始传。过度乐观的进度条会在末端戛然而止比缓慢的进度条更让人难受。第二个技巧是底部追加一条已上传大小/总大小的实时数字。这条文字数据不依赖分片状态而是把XHR的e.loaded累加起来虽然不够精确但用户能直观看到数字在涨心理上会觉得系统在干活。实测下来用户的焦虑感会大幅降低。第三个技巧是合并阶段单独显示正在合并文件。因为分片全部传完后合并阶段可能还需要几秒钟甚至几十秒取决于文件大小和磁盘速度如果进度条直接卡在100%不动用户会以为完成了但服务器其实还在忙。所以我会把进度条分为两段上传阶段走到95%最后5%留给合并中这个状态文案也换成正在合并文件请稍候。5. 断点续传、秒传与进度条的关系让进度变得可持续进度条能显示当下进度这还不够。大文件上传的体验里更重要的其实是进度可恢复——用户传了80%网络断了刷新页面能不能直接从80%继续而不是从头再来。这就引出了断点续传和秒传。5.1 已上传分片探测与断点续传断点续传的实现基础是前端能知道哪些分片已经传到了服务器。做法是前端上传前先调用一个探测接口把这个文件的唯一标识一般用文件内容的前面几MB算一个特征值或者直接用整个文件的MD5发给后端后端扫描临时目录返回已存在的分片序号列表。前端拿到这个列表后只需要把未上传的分片加入任务队列// check_uploaded_chunks.php $uploadId $_GET[upload_id] ?? ; $uploadDir __DIR__ . /upload_temp/ . $uploadId; $uploaded []; if (is_dir($uploadDir)) { $files scandir($uploadDir); foreach ($files as $file) { if (preg_match(/^(\d)\.part$/, $file, $matches)) { $uploaded[] (int)$matches[1]; } } } echo json_encode([code 0, uploaded_chunks $uploaded]);前端拿到uploaded_chunks后过滤掉这些分片再跑上传队列这样进度条一开始就不是0%而是直接从已上传的分片数开始。这有一个额外的好处如果用户上次传到80%中断了这次打开页面进度条直接从80%开始走体验拉满。5.2 秒传时的进度条处理逻辑秒传是断点续传的进阶版。后端如果发现这个文件已经完整存在比如曾经有人传过且文件名相同、内容特征值相同就不需要再接收分片直接告诉前端这文件已经传过了。这种情况下进度条的处理逻辑很简单不是从0%跑到100%而是从0%直接跳到100%或者使用动画快速滚一遍。如果项目里对上传有审计需求这个秒传动作也要记一条日志记录哪个用户、什么时间、通过秒传完成了哪个文件的上传。我见过有项目在秒传时给用户弹了个已通过秒传完成用户还挺惊喜因为几秒钟就传完了几个G的文件。需要注意的点是特征值计算要放在前端做而且要在用户选择文件后立刻触发不要等所有分片上传完了才发现可以秒传。正常流程是选择文件 - 计算特征值 - 调探测接口 - 如果返回已存在直接走秒传否则走分片上传。5.3 并发数、分片大小与服务端超时的联动调整这一节可能是这篇文章里最经验值的部分。参数之间是联动的不要只调其中一个。先说并发数。我把并发数定为3这个数字不是拍脑袋。浏览器对同一域名的并发连接数限制大约是6个如果分片上传独占这6个连接其他请求比如页面里的图片、API调用就会排队体验很差。所以并发数设置在2~4之间比较合理我习惯取3。并发数定了分片大小的上限也就定了。假设并发3每个分片5MB同时最多15MB在途数据。如果用户上行带宽只有10Mbps这15MB会让链路排满其他网络活动全部卡顿。所以要结合目标用户群的上传带宽来微调。我的建议是分片大小 × 并发数 * 8bit和Byte的换算不大于用户带宽的80%。举个例子如果用户是20Mbps上行那么分片大小乘并发数不应超过20 * 0.8 / 8 2MB。这显然太保守了所以实际项目里需要做一个简单的带宽自适应如果某个分片上传耗时超过3秒就把并发数降为2或者分片大小减半。服务端超时也要配合。分片接口的PHP执行时间很短接收一个5MB的文件并保存通常几十到几百毫秒所以max_execution_time维持30秒足够。但post_max_size要调大至少是分片大小的两倍因为FormData里还带着几个字段加上分片本身的体积如果上限太紧大一点的分片会莫名其妙失败。6. 实测踩坑记录从进度条乱跳到文件损坏的排查过程最后这一节我来完整复盘一个真实的排查过程。这是我在某图像处理Demo项目里遇到的问题现象很典型。6.1 进度条回退和抖动的真实原因上线后的第一个反馈是进度条到了47%突然跳回40%然后又慢慢升到41%、42%。用户以为上传出Bug了。排查下来的根因有两个。第一个原因是分片上传失败后的重试逻辑里finishedChunks没有做到原子性。我的代码里每个分片成功后会执行finishedChunks如果本来并发3个请求其中两个成功了一个失败了要重试而重试期间前端又把进度算了一遍finishedChunks的值就出现了一个瞬间的少计数而不是多计数进度条自然往回跳。这在浏览器的单线程JS里理论上不会发生但如果你用了Web Worker或者把finishedChunks放在异步回调里更新时没有加锁就可能出现这个竞态。我的修复方式是不在回调里直接累加而是改为每完成一个分片就重新统计一次。用一个Map保存已完成分片的index进度统计时看这个Map的大小而不是依赖一个整数计数器的自增。第二个原因是缓存。我在前端用了fetch的默认缓存策略状态接口的响应偶尔被浏览器缓存前端轮询到的进度是旧的看起来就像进度变慢或卡住。修复方法是给状态接口加Cache-Control: no-store前端请求时再加个时间戳参数防止缓存。6.2 合并时内存暴涨与超时第二个真实问题是分片都传完了合并接口却报500或者PHP直接崩了。看日志发现是内存不足。前面我给的merge.php示例代码用了file_get_contents加fwrite这在分片小的时候没问题。但当分片是10MB总文件达到1GB时PHP进程内存虽然不会一次性吃掉1GB因为是循环读取分片但每个分片10MB的file_get_contents加上其他开销累积起来也可能触发内存限制尤其在PHP配置里memory_limit只有128MB的默认值下。我的修复方式是改用流式合并用fopen配合stream_copy_to_stream一次只拷贝一个分片流PHP内存占用几乎恒定为几MB$destFile __DIR__ . /uploads/ . $uploadId . _ . basename($originalName); $destFp fopen($destFile, wb); for ($i 0; $i $totalChunks; $i) { $partFile $uploadDir . / . $i . .part; $partFp fopen($partFile, rb); stream_copy_to_stream($partFp, $destFp); fclose($partFp); unlink($partFile); } fclose($destFp);还有一个超时问题。如果合并1GB的文件磁盘写入耗时可能超过30秒PHP的max_execution_time默认30秒会导致合并中断。合并接口顶部需要加set_time_limit(0)或者至少设置为300秒。这里我提醒一下set_time_limit(0)在生产环境要慎用如果合并逻辑有死循环PHP进程就永远不退了。更好的做法是设置一个合理的上限比如300秒并且把合并任务放到后台队列执行前端用轮询查询合并状态。6.3 浏览器刷新后进度归零的恢复方案还有一个日常高频问题用户上传到一半不小心刷新了页面进度条归零但后端的临时分片其实都还在。这个问题的解法其实前面已经铺垫过了。刷新后重新选择文件前端立刻发起探测请求把uploadId对应的已上传分片列表拿回来就可以接着上传。但有个痛点如果uploadId是刷新前用Date.now()生成的刷新后就找不回来了。所以要实现完整的断点续传文件的唯一标识不能用随机的uploadId而要用基于文件内容特征值生成的上传标识。比如取文件前8MB的MD5值作为uploadId这样刷新后重新计算特征值就能定位到之前上传了一半的临时目录。代价是同一时间只能有一个会话在传同一个文件不然会互相覆盖分片。解决这个并发冲突的办法也很简单uploadId用文件特征值 用户ID 会话ID的组合但把组合结果用一个哈希函数算成固定长度字符串。我的个人经验是这种功能在真实产品中属于要有但不能太复杂的级别。如果团队人手有限可以先只做断点续传的探测与续传不做多端同时上传同一文件的锁机制并在产品文档里说明同一文件请勿在多个标签页同时上传。再分享一个小技巧所有上传参数分片大小、并发数、是否启用断点续传、是否启用秒传尽量做成后台可配置的开关不要写死在代码里。我见过不少项目上线后才发现某个特定网络环境下参数需要调整被迫发版才能改非常被动。做成配置之后遇到问题改一下配置就能平滑应对这是经历过生产环境毒打之后养成的习惯。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。