AJAX上传时readyState==3无法重复触发的原因咨询
为什么上传场景下XMLHttpRequest的readyState=3无法重复触发?
咱们先对比下你的下载和上传场景的核心差异,就能找到问题所在了:
先看下载场景为什么能正常工作
你的下载逻辑是客户端发GET请求给PHP,PHP通过cURL拉取远程大文件,同时实时输出进度:
ob_start(); ob_flush(); flush(); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://example.com/big-video-file.mp4"); curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, 'progress'); curl_setopt($ch, CURLOPT_NOPROGRESS, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); curl_close($ch); echo 100; ob_flush(); flush(); function progress($resource,$download_size, $downloaded, $upload_size, $uploaded) { if ($download_size > 0) echo round(($downloaded / $download_size) * 100).','; ob_flush(); flush(); }
客户端的XHR能多次触发readyState=3,是因为:
- 客户端发送的GET请求非常轻量,服务器立刻开始处理。
- PHP在拉取文件的过程中,通过
ob_flush()和flush()把进度实时推送给客户端,没有等待整个下载完成才输出。 - 浏览器接收到服务器分批返回的响应内容,就会重复触发
readyState=3(表示已接收部分响应)。
对应的客户端代码也能正常捕获这些进度:
let req = new XMLHttpRequest(), ta = document.querySelector('textarea'); req.open('GET', 'curl_progress.php?ajax=1'); req.addEventListener('readystatechange', function() { if (req.readyState == 3) { //we get here repeatedly - yay! let spl = req.responseText.split(','); ta.value += spl[spl.length-2]+'\n'; } else if (req.readyState == 4 && req.status == 200) ta.value += 100; }, false); req.send();
上传场景的问题根源
再看你的上传逻辑:客户端先把文件通过FormData上传到PHP,PHP接收完文件后,再用cURL转发到Vimeo:
$ch = curl_init($_SESSION['vimeo_upload_url']); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_POST => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_INFILE => fopen($_FILES['file']['tmp_name'], 'r'), CURLOPT_INFILESIZE => $_FILES['file']['size'], CURLOPT_UPLOAD => true, CURLOPT_PROGRESSFUNCTION => 'curl_progress', CURLOPT_RETURNTRANSFER => true, CURLOPT_NOPROGRESS => false, CURLOPT_SSL_VERIFYPEER => false ]); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Tus-Resumable: 1.0.0', 'Upload-Offset: 0', 'Content-Type: application/offset+octet-stream' ]); function curl_progress($resource, $download_size, $downloaded, $upload_size, $uploaded) { if ($upload_size > 0) echo round(($uploaded / $upload_size) * 100).','; ob_flush(); flush(); }
这里有两个关键的阻塞点导致readyState=3只触发一次:
- 客户端到PHP的上传是同步阻塞的:你用的是
FormData发送multipart/form-data请求,浏览器会先把整个文件完整发送给PHP,PHP必须接收完整个文件(存在$_FILES['file']['tmp_name']里)之后,才会开始执行cURL转发到Vimeo的逻辑。 - 进度输出时机滞后:当PHP开始输出转发进度时,客户端已经完成了文件上传,此时XHR处于“等待服务器返回完整响应”的状态。即使你用了
ob_flush()和flush(),服务器(比如nginx/Apache)的缓冲机制可能会把这些进度内容暂时缓存,直到接近转发完成时才一次性推送给客户端,所以readyState=3只会触发一次。
对应的客户端代码自然只能捕获一次进度:
let req = new XMLHttpRequest(), fd = new FormData(); fd.append('file', input.files[0]); req.open('POST', '../vimeo_upload.php?action=transfer_file'); req.addEventListener('readystatechange', function() { if (this.readyState == 3) { //we get here only once! let spl = this.responseText.split(','); console.log(spl[spl.length-2]); } else if (this.readyState == 4 && this.status == 200) console.log(100); }, false);
解决办法
要实现实时的上传进度,你可以从以下几个方向调整:
1. 拆分请求:先上传文件,再查转发进度
- 第一步:客户端用XHR上传文件到PHP,此时可以用XHR原生的
progress事件监听客户端到PHP的上传进度。 - 第二步:文件上传完成后,客户端发起一个新的GET请求,PHP在这个请求里执行cURL转发到Vimeo,并实时输出进度,此时就能像下载场景那样重复触发
readyState=3了。
2. 用WebSocket实时推送进度
改用WebSocket代替XHR,PHP在执行cURL转发时,通过WebSocket把进度推送给客户端。这种方式不受HTTP响应缓冲的限制,能更实时地获取进度。
3. 直接从客户端上传到Vimeo(最推荐)
如果Vimeo支持直接上传(比如它的Tus协议上传),可以让客户端直接和Vimeo的上传接口通信,这样客户端可以直接监听客户端到Vimeo的上传进度,既省去了PHP中转的开销,进度也更准确。
4. 调整服务器缓冲配置(临时方案)
如果你坚持要PHP中转,可以尝试关闭服务器的缓冲:
- PHP端:确保
php.ini里的output_buffering = Off,并且脚本开头没有开启额外的缓冲。 - Nginx端:在站点配置里添加
proxy_buffering off;,避免nginx缓冲PHP的输出。 - Apache端:确保没有开启
mod_deflate对该请求的压缩(压缩会强制缓冲)。
内容的提问来源于stack exchange,提问作者Mitya
相关产品推荐
相关产品推荐

