直接通过 file:// 协议打开包含上传功能的 HTML 页面时,很多逻辑会变得不可靠:点选文件后,进度条要么停在 0%,要么直接触发 error 回调。核心原因并不在 jQuery,而在于浏览器对本地文件页面的网络请求以及进度事件做了严格限制。标准上传流程中,XMLHttpRequest 的 upload.onprogress 会在请求体逐步发送到服务器时持续触发,但在 file:// 页面里,这个发送动作往往不会真实发生,所以进度事件也不会产生。一个可行的思路是借助 jQuery 的 $.ajaxTransport 扩展机制,拦截这类上传请求,在本地用定时器或分片读取来模拟数据消费过程,并把模拟出的 loaded、total、percent 回传给进度回调。

为什么 file:// 下拿不到上传进度
真实上传的进度由浏览器网络栈在发送请求体时产生。file:// 页面本身不是通过 HTTP 服务器加载的,浏览器的同源策略和安全沙箱会阻止像 XMLHttpRequest 这样的对象向 http:// 或 https:// 地址发送带本地文件内容的请求;即便某些浏览器允许发送,也常常处于受限模式,upload.onprogress 不被触发。你可以把文件选择、本地读取、网络上传理解为三个独立阶段,<input type="file"> 只负责拿到 File 引用,FileReader 只负责把内容读进内存,而真正的上传进度只发生在网络阶段。file:// 下网络阶段缺失,进度自然没有来源。
还有一个容易混淆的地方:FileReader 的 onprogress 虽然能反映读取本地文件的进度,但它更新的是“读取多少字节到内存”,而不是“上传了多少字节”。如果直接把 FileReader 的进度当成上传进度展示,会出现进度条很快走完,但后续提交尚未开始的错位感。用 jQuery 的 $.ajax 直接传 FormData 在 file:// 页面中也极有可能直接失败,因为底层 xhr 无法完成发送。要解决这个问题,就需要在 jQuery 的 AJAX 管道中插入自定义的传输层,把请求“截下来”,按自己的节奏推进度。
$.ajaxTransport 在请求管道中的角色
jQuery 的 $.ajax 并不是直接调用 XMLHttpRequest,而是先经过 prefilter、converter、transport 等环节。transport 是最终负责把请求发出去并接收响应的组件,默认情况下 jQuery 会根据 url 协议选择 xhr 或 script 等传输器。我们可以通过 $.ajaxTransport('+mock-upload', function(options, originalOptions, jqXHR) {...}) 注册一个新的传输器。参数中 '+mock-upload' 的加号表示只要请求的 dataType 包含 mock-upload 字符串,就轮到这个传输器处理。
当 jQuery 匹配到一个自定义传输器后,会调用它返回对象的 send 方法,把请求头和完成回调传进去;如果用户调用 abort(),则会触发返回对象里的 abort 方法。因此我们在 send 里完全不用发网络请求,而是启动一个模拟循环;在循环中根据文件大小推进 loaded,并把计算好的事件对象回传给 originalOptions.progress;最后调用 completeCallback 结束这次请求。这样外层 $.ajax 的 done、fail、progress 等回调可以继续工作,业务代码几乎不需要改动。
下面先给出一个最小的 transport 结构,帮助理解它如何接管请求:
$.ajaxTransport('+mock-upload', function(options, originalOptions, jqXHR) {
return {
send: function(headers, completeCallback) {
// 实际不做网络请求,直接告诉 jQuery 请求成功
completeCallback(200, 'success', { text: 'ok' });
},
abort: function() {
// 请求被取消时调用
}
};
});
完整实现:在 file:// 页面中模拟本地文件上传进度
首先在页面中放置一个文件选择控件、一个按钮和一个进度条,结构大致如下:<input type="file" id="fileInput">、<button id="uploadBtn">上传</button> 以及一个用于展示进度的 <div>。接着在 JavaScript 里监听文件变化,把选中的 File 对象保存到变量中。点击上传按钮后构造一个 FormData,将文件 append 进去,并调用 $.ajax,同时把 dataType 设置为 mock-upload。
自定义 transport 的核心任务是从 FormData 中提取 File 对象,并拿到文件总大小。由于 FormData 没有公开的 get 方法,遍历 entries 迭代器是兼容性较好的做法。取到 file 后,用 file.size 作为 total 值,然后启动定时器,每次增加一个 chunk。chunk 可以取总大小的六十分之一左右,这样进度条大约三秒钟走完,观感比较自然。每次更新后用 originalOptions.progress 把自定义事件对象传出去,事件对象中应包含 loaded、total、lengthComputable 和 percent 四个关键字段。
下面是完整的 transport 注册代码:
$.ajaxTransport('+mock-upload', function(options, originalOptions, jqXHR) {
var timer = null;
return {
send: function(headers, completeCallback) {
var file = null;
if (originalOptions.data instanceof FormData) {
var entries = originalOptions.data.entries();
var entry = entries.next();
while (!entry.done) {
if (entry.value[1] instanceof File) {
file = entry.value[1];
break;
}
entry = entries.next();
}
}
if (!file) {
completeCallback(400, 'error', { text: '未找到文件对象' });
return;
}
var total = file.size;
var loaded = 0;
var chunk = Math.max(Math.ceil(total / 60), 1024);
timer = setInterval(function() {
loaded += chunk;
if (loaded > total) {
loaded = total;
}
var event = {
loaded: loaded,
total: total,
lengthComputable: true,
percent: total ? (loaded / total * 100) : 100
};
if (originalOptions.progress && typeof originalOptions.progress === 'function') {
originalOptions.progress(event);
}
if (loaded >= total) {
clearInterval(timer);
timer = null;
completeCallback(200, 'success', { text: '模拟上传完成' });
}
}, 50);
},
abort: function() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
};
});
外层调用代码同样简单。选择文件后,点击按钮发起 ajax 请求,在 progress 回调里用返回的 percent 更新进度条样式,在 done 里显示完成状态。这样整个流程就从“真实发送请求”切换成了“本地模拟发送”,但业务侧感知不到差异。
$('#uploadBtn').on('click', function() {
var fileInput = $('#fileInput')[0];
if (!fileInput.files || !fileInput.files[0]) {
alert('请先选择文件');
return;
}
var fd = new FormData();
fd.append('file', fileInput.files[0]);
$.ajax({
url: 'mock://local-upload',
type: 'POST',
data: fd,
dataType: 'mock-upload',
progress: function(e) {
var percent = 0;
if (e.total) {
percent = Math.round(e.loaded / e.total * 100);
}
$('#progressFill').css('width', percent + '%');
$('#progressText').text(percent + '%');
}
}).done(function(res) {
$('#status').text('上传完成:' + res.text);
}).fail(function(xhr, status, error) {
$('#status').text('上传失败:' + error);
});
});
注意事项与适用边界
模拟进度只适用于没有真实服务端的本地原型、离线演示或 Electron 早期阶段。生产环境务必替换为 HTTP 上传,因为真实上传进度能反映网络波动、服务端反压等实际状态。模拟进度虽然能带来完整交互体验,但它会掩盖一些潜在问题,例如后端接收失败、超时、跨域限制等,因此不要把模拟逻辑留在正式代码中。
对于大文件场景,定时器模拟并不实际读取文件内容,所以不会造成内存压力,进度速度也是固定的。如果希望进度更贴近真实文件的读取节奏,可以改用 FileReader 的分片读取方式,每次读取一个 slice,在读取完成后手动推进 loaded。不过 FileReader 是异步的,需要写成递归或链式调用,并且在 file:// 环境下读文件可能仍受到安全策略限制。
此外还需要注意几点:自定义 dataType 不要与真实后端的 dataType 冲突;abort 时必须清理定时器,避免定时器继续运行并更新已经取消的请求;多个文件可以分别模拟,也可以按文件总大小合并计算;file:// 页面在 Chrome、Edge、Firefox 中的行为略有差异,建议在本地启动 HTTP 服务做更稳定的测试,例如使用 python -m http.server 或 npx serve 启动静态服务。
jQuery ajaxTransport本地文件上传上传进度模拟修改时间:2026-09-18 19:07:24