高性能进度组件可用原生技术实现,关键在逻辑与ui分离、避免重复绑定、正确处理事件生命周期和内存泄漏。

直接用 <input type="file"> + XMLHttpRequest + 原生 DOM 更新就能做出高性能进度组件,不需要框架、不依赖第三方库,关键在剥离 UI 与逻辑、避免重复绑定、正确处理事件生命周期。
为什么不能把进度逻辑写死在 HTML 模板里
HTML 模板本身不执行逻辑,所谓“封装”不是往 <template></template> 里塞 JS,而是定义可复用的结构 + 明确的挂载契约。常见错误是把 xhr.upload.onprogress 回调硬编码进模板字符串,导致每次实例化都新建闭包、监听器无法清理、event.target.files 引用错乱。
- 模板只负责提供
<input id="uploader-input">、<div class="progress-bar">、<code><span class="percent"></span>这类语义化占位节点 - 真实逻辑必须由外部 JS 控制:绑定一次
change事件、为每个文件创建独立XMLHttpRequest实例、手动更新对应进度节点 - 多文件上传时,每个文件应有独立进度容器,否则多个
onprogress会互相覆盖 DOM - 必须写成
if (e.lengthComputable) { ... },否则进度条永远卡在 0% - 降级方案不是“猜总大小”,而是改用状态提示(如“正在发送…”“已连接服务器”),而不是强行显示百分比
- 测试时务必用真实后端环境,
localhost下某些浏览器(尤其 Safari)对本地服务响应头更严格 - 单文件:直接取
files[0],用FormData.append('file', file) - 多文件:遍历
files,每次新建FormData并append('files', file),确保后端字段名一致 - 分片上传:先用
file.slice(start, end)切块,每块生成独立FormData,附带append('chunkIndex', i)和append('totalChunks', n),服务端据此合并 - 所有模式共用同一套进度更新逻辑,只是数据源(
file.sizevsfile.size / chunkSize)和请求目标(/uploadvs/upload/chunk)不同 -
input元素每次选新文件,旧的change监听器不会自动解绑——必须在绑定前先removeEventListener,或用一次性监听({ once: true }) - 每个
XMLHttpRequest实例必须在load或error后显式置null,否则闭包持有对 DOM 节点的引用,GC 不回收 - 大文件预览时若用
readAsDataURL,base64 字符串会吃掉数倍内存;超过 5MB 的图建议改用URL.createObjectURL(file),上传完成后立即URL.revokeObjectURL()
XMLHttpRequest.upload.onprogress 必须检查 event.lengthComputable
这个布尔值决定 event.total 是否可信。它为 false 的典型场景包括:服务端没返回 Content-Length(如 Nginx 默认不透传)、用了代理但未配置 proxy_buffering off、后端用流式响应但没设 Content-Length 头。此时 event.total === 0,直接算百分比会得到 NaN 或 Infinity。
如何让一个组件支持单文件 / 多文件 / 分片上传三种模式
核心是把“上传动作”抽象成函数工厂,根据参数动态生成行为,而不是写三套逻辑。
容易被忽略的内存与事件泄漏点
高频上传场景下,不清理会导致页面缓慢甚至崩溃。
真正难的不是写出能动的进度条,而是让组件在连续上传 20 次、切换 5 种文件类型、中途取消 3 次之后,DOM 节点数不变、内存占用平稳、进度数值不跳变——这取决于你是否在每次 new XMLHttpRequest() 后都记住了它的存在,并在恰当时候亲手把它了结掉。











