必须用 XMLHttpRequest 监听下载进度,因其支持 onprogress 事件和 responseType='blob' 组合获取 loaded/total 字节数;fetch 不支持;需防抖更新进度条、用 URL.createObjectURL 创建 Blob 下载地址并及时释放,iOS Safari 需降级为服务端直链下载。

用 XMLHttpRequest 监听下载进度,不是 fetch
浏览器原生 fetch 不支持监听下载进度(ReadableStream 可读但无法获取总大小),必须退回 XMLHttpRequest。它提供 onprogress 事件和 responseType = 'blob' 组合,才能拿到已下载字节数和总字节数。
常见错误是直接对 fetch 调用 .then(res => res.blob()) 后再处理——这时进度早已结束,无法监听。
-
XMLHttpRequest必须显式设置responseType = 'blob',否则response是字符串,response.length无意义 - 请求头不能加
Content-Length(服务端返回才有效),前端无法控制;重点看响应头是否含Content-Length或Content-Range - 若服务端没返回
Content-Length(比如动态生成文件、流式响应),event.total为 0,进度条只能显示“下载中”,无法计算百分比
进度条 UI 更新要防抖 + 判重
高频 onprogress 事件(尤其大文件)可能每秒触发几十次,直接更新 DOM 会卡顿。不要每次事件都调用 element.style.width = ...。
- 用
requestAnimationFrame节流:只在下一帧更新一次,视觉平滑且不丢精度 - 加简单判重:仅当计算出的百分比变化 ≥ 1% 再更新,避免小数点抖动
- 别用
innerHTML插入数字——文本节点更新比样式变更更耗性能;优先改textContent或 CSS 变量
示例关键片段:
立即学习“前端免费学习笔记(深入)”;
let lastPercent = -1;
xhr.onprogress = (event) => {
if (!event.lengthComputable) return;
const percent = Math.round((event.loaded / event.total) * 100);
if (percent === lastPercent) return;
lastPercent = percent;
requestAnimationFrame(() => {
progressBar.style.width = `${percent}%`;
progressText.textContent = `${percent}%`;
});
};下载完成后的 Blob 处理不能漏 URL.createObjectURL
拿到 xhr.response(Blob)后,不能直接用 URL.href = blob——Blob 是内存对象,没有 URL。必须用 URL.createObjectURL(blob) 创建临时地址,再触发下载。
- 创建的 URL 必须在下载完成后手动
URL.revokeObjectURL(url),否则内存泄漏(尤其反复下载时) -
a.download属性只在同源时生效;跨域下载会忽略该属性,直接打开新页或另存为,且文件名由服务端Content-Disposition决定 - 如果服务端没设
Content-Disposition: attachment; filename="xxx.pdf",浏览器可能按路径推断文件名(如/api/download?id=123→123),建议后端补上
移动端 Safari 的 download 属性完全失效
iOS/iPadOS Safari 不支持 <a download> 下载 Blob URL,点击后会打开空白页或报错 Failed to load resource: Frame load interrupted。这是硬限制,无绕过方案。
- 唯一可行方式:服务端生成带完整文件名的直链(如
https://example.com/files/report-2024.pdf),并确保响应头含Content-Disposition: attachment - 前端可 fallback:检测
navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome'),走跳转而非 Blob 方案 - 注意:即使服务端直链,iOS 仍可能拦截自动下载(尤其是非用户手势触发),所以按钮必须是真实点击,不能用
setTimeout延迟触发
真正麻烦的不是写法,而是不同环境对“下载”的定义根本不一致——PC Chrome 认为 Blob + download 是下载,iOS Safari 认为只有服务端直链才算下载。这个边界得提前跟后端对齐,别等联调才发现文件名丢了。



















