You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,是因为:

  1. 客户端发送的GET请求非常轻量,服务器立刻开始处理。
  2. PHP在拉取文件的过程中,通过ob_flush()和flush()把进度实时推送给客户端,没有等待整个下载完成才输出。
  3. 浏览器接收到服务器分批返回的响应内容,就会重复触发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只触发一次:

  1. 客户端到PHP的上传是同步阻塞的:你用的是FormData发送multipart/form-data请求,浏览器会先把整个文件完整发送给PHP,PHP必须接收完整个文件(存在$_FILES['file']['tmp_name']里)之后,才会开始执行cURL转发到Vimeo的逻辑。
  2. 进度输出时机滞后:当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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:40:24