为每个文件单独创建 XMLHttpRequest 实例并绑定独立 onprogress 回调,避免复用导致进度错乱或 InvalidStateError;动态生成对应 DOM 节点,用闭包保存索引;超大文件需加停滞检测 fallback。

多个 XMLHttpRequest 实例对应每个文件
单个 XMLHttpRequest 只能监听一次上传的进度,无法区分多个文件。要分别显示每个文件的进度,必须为每个 File 对象单独创建一个 XMLHttpRequest 实例,并绑定各自的 xhr.upload.onprogress 回调。
常见错误是复用同一个 xhr 对象循环发送 —— 这会导致进度事件全部打在最后一个文件的 UI 元素上,或直接报错 InvalidStateError(因 xhr 已进入完成状态)。
- 每选中一个文件,就动态生成一个包含独立
progress元素、status文本和取消按钮的 DOM 节点 - 在
uploadFile(file, index)函数中 new 一个XMLHttpRequest,并把index或文件名作为闭包变量传入onprogress回调 - 避免用
for...in遍历files,改用for (let i = 0; i ,否则容易因变量提升导致所有回调共享同一个 <code>i
FormData.append() 的 key 名要带索引或唯一标识
后端接收多文件时,如果所有文件都用相同字段名(如 formData.append('file', file)),部分服务端框架(如 Express + Multer)会覆盖前一个,只剩最后一个文件。必须让每个文件的字段名可区分。
推荐写法:formData.append('files[' + i + ']', file, file.name) 或 formData.append('file_' + Date.now() + '_' + i, file)。这样后端能按名解析,前端也能靠字段名映射回对应进度条。
立即学习“前端免费学习笔记(深入)”;
注意:第二个参数 file 是 File 对象,第三个参数(可选)是提交时使用的文件名,它会影响 Content-Disposition 头,对后端日志/调试有帮助。
进度条 DOM 必须提前挂载且可定位
不能等 onprogress 触发了才插入进度条 —— 那样会造成视觉跳动、事件绑定延迟,甚至因异步时机问题导致更新错位。
正确做法是在用户选择文件后、点击上传前,就遍历 files 并为每个文件生成一套 UI 结构:
- 一个
<div class="file-item">容器,含文件名、大小、<progress>、<span class="percent">和取消按钮 - 给每个容器加
data-index="i"或data-file-id,方便上传回调中精准更新 - 禁用上传按钮直到所有 DOM 渲染完毕,防止用户重复触发
示例片段:document.querySelector('[data-index="' + i + '"] .percent').textContent = Math.round(percent) + '%';
取消上传要用 xhr.abort(),别只删 DOM
仅移除页面上的进度条或隐藏元素,不会终止实际网络请求,该文件仍在后台上传,浪费带宽且可能干扰后续请求。
每个 xhr 实例需保存到数组或 Map 中(如 const xhrMap = new Map(); xhrMap.set(fileId, xhr);),点击“取消”时查出对应实例并调用 xhr.abort(),再清理 DOM 和 Map 条目。
注意:abort() 会触发 xhr.onerror,但不会触发 onload 或 onloadend;你得在 onerror 里判断 xhr.status === 0 && xhr.readyState === 0 来区分真实错误和主动取消。
xhr.upload.onprogress 在某些旧版 Safari 或 Electron 内核中对 >2GB 文件可能不触发或数值异常**。如果业务涉及超大文件,必须加 fallback 判断(比如定时检查 xhr.upload.loaded 是否停滞超过 10 秒),不能只信进度事件。



















