前端常见文件下载方式总结
前言
最近在维护一个老项目,为其新加了一个文件批量下载功能,但是遇到一个隐藏的bug,具体表现就是谷歌浏览器用 xhr 同时下载超过10个小文件时,最后只保存下来10个,观察调试工具的网络请求面板,发现文件也确实都请求到了,浏览器设置里不管开没开自动保存到默认文件夹选项,最后本地都会缺失一部分文件。
刚开始以为控制xhr请求频率就能解决,遂将并发请求改为同步请求,结果还是一样,又在每个请求中间加了1s延迟,也不起作用。这问题就显而易见了,不是网络请求出了问题,是在文件保存阶段浏览器限制了同时保存数量。因为文件都是几M内的小文件,几乎是瞬间下载完成的,但是在持久化到本地文件的时候,浏览器会表现得有点迟钝,大概就是在这个阶段把超过10个文件上限的文件直接丢弃了,至于为什么单窗口同时最多只能同时保存10个文件,ChatGPT给出的答案是Chrome浏览器并发请求数和文件句柄的限制。
接下来解决的方向就是怎么突破单窗口同时保存文件的数量限制了,以下方式我几乎试了个遍,简单总结了每种方式的优缺点,只试验了49以上版本谷歌浏览器,可能不同的浏览器环境略有差异。
前端常见的几种文件下载情形:单文件下载、多文件下载、大文件分片下载、多文件打包合并下载等。
下载过程可分两步:触发请求和保存文件,并且通常需要后端接口配合鉴权和响应类型(Content-Type)的协商。
后端接口支持
- 设置响应头:
Content-Disposition: attachment;filename={你的文件名}
,可防止浏览器直接预览文件,其次,可设置默认保存文件名,防止浏览器以URL中的最后一个路径部分(即文件名)来命名文件。 - 建议后端设置响应头
Content-Length
,此举可使前端观测到下载进度。 - 下载地址最好支持GET请求。
- 也可通过 Server-Sent Events (SSE) 推送文件下载状态。
触发请求
假设你现在有一个文件下载地址: https://example.com/static/file.txt
在浏览器中有以下几种触发下载请求的方案:
1. window.location.href
const url ='https://example.com/static/file.txt'
window.location.href =`${url}&token=${getToken()}`
优点:
- 有进度条,可取消、暂停
缺点:
- 一次只能下载一条,不支持批量下载
- 仅支持GET请求
- 无法获取文件下载成功的事件
适合单文件下载场景
2. 标签
functiondownloadFile(url){const anchor = document.createElement('a');
anchor.href = url;
anchor.download = url.split('/').pop();// 设置文件名
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);}
优点:
- 有进度条,可取消、暂停
- 可同时并发下载多个
缺点:
- 仅支持GET请求
- 无法获取文件下载成功的事件
- 存在兼容性问题
- 无法突破浏览器最多同时保存10个文件的限制,超出部分即使下载完成也会丢弃。
如果响应头中没有设置Content-Disposition为
attachment
,使用a标签的 download 属性也可让浏览器下载该文件,且以该属性命名文件,而不是尝试预览文件。
如果响应头中设置了Content-Disposition为
attachment
,使用a标签的 download 属性可以在重命名该下载文件名。
3. XMLHttpRequest\fetch + 标签
这两种请求方式都是请求成功后,先将文件内容缓存到内存的blob对象中,然后再持久化到文件中,类似的还有axios
fetch:
functiondownloadFile(url, filename){fetch(url,{method:"GET"}).then(response=>{if(!response.ok){thrownewError(`HTTP error! status: ${response.status}`);}return response.blob();}).then(blob=>{const downloadUrl = window.URL.createObjectURL(blob);const anchor = document.createElement('a');
anchor.href = downloadUrl;
anchor.download = filename ||'downloaded_file';// 设置文件名
document.body.appendChild(anchor);
anchor.click();
window.URL.revokeObjectURL(downloadUrl);// 释放 URL 对象
console.log('Download completed successfully');}).catch(error=>{
console.error('Failed to download file', error);});}// 调用函数下载文件downloadFile('https://example.com/file.zip','myFile.zip');
XHR:
functiondownloadFile(url, filename){const xhr =newXMLHttpRequest();
xhr.open('GET', url,true);// 设置 responseType 为 'blob',表示将响应数据作为二进制数据处理
xhr.responseType ='blob';// 监听下载进度事件
xhr.onprogress=function(event){if(event.lengthComputable){const percentComplete =(event.loaded / event.total)*100;
console.log(`Download progress: ${percentComplete.toFixed(2)}%`);}};// 监听下载完成事件
xhr.onload=function(){if(xhr.status ===200){const blob = xhr.response;const downloadUrl = window.URL.createObjectURL(blob);const anchor = document.createElement('a');
anchor.href = downloadUrl;
anchor.download = filename || url.split('/').pop();
document.body.appendChild(anchor);
anchor.click();// 释放 URL 对象
window.URL.revokeObjectURL(downloadUrl);}else{
console.error('Download failed', xhr.statusText);}};// 监听请求错误事件
xhr.onerror=function(){
console.error('Request error occurred');};
xhr.send();}// 调用函数下载文件downloadFile('https://example.com/file.zip','myFile.zip');
优点:
- 可同时并发下载多个
- 支持异步请求
- 支持GET、POST等请求
- 可明确获取文件下载(缓存)进度
缺点:
- 无法突破浏览器最多同时保存10个文件的限制,超出部分即使下载完成也会丢弃。
- 文件会先全部缓存到内存中,所以下载大文件会使机器卡顿。
适合10个以内的多文件同时下载,且文件内容都比较小.
下载进度事件依赖
Content-Length
响应头。
4. window.open
打开新窗口下载文件,如果后端设置了响应头
Content-Disposition: attachment;
, 会触发一个下载任务,而不是在浏览器中打开该内容,然后会立即关闭该窗口。
functiondownloadFileInBackground(url){const newWindow = window.open(url,'_blank');if(newWindow){// 在新窗口打开后,立即关闭窗口
newWindow.onload=function(){
newWindow.close();};}else{
console.warn("下载弹窗被拦截!请开启权限");}}// 调用下载函数downloadFileInBackground('https://example.com/file.zip');
优点:
- 有进度条,可取消、暂停
- 没有下载数量限制,不受浏览器最多同时保存10个文件的限制。
缺点:
- 仅支持GET请求
- 无法监听文件下载进度
- 打开新窗口(弹窗)可能会被拦截
适合单文件下载、任意数量的多文件下。建议控制同时下载的数量,否则会同时打开过多空窗口,用户体验不太友好。
弹窗容易被拦截,注意提示用户打开弹窗权限。
5. iframe
let iframe = document.createElement('iframe');
iframe.src ='https://example.com/static/file.txt'
iframe.style.display ='none';
document.body.appendChild(iframe);
iframe.addEventListener('load',function(){ document.body.removeChild(iframe);});
优点:
- 有进度条,可取消、暂停
- 没有并发下载数量限制,不受浏览器最多同时保存10个文件的限制。
缺点:
- 仅支持GET请求
- 无法明确文件下载完成时机
保存文件
全量下载,先下后存
通过了解上面列出的不同触发下载方式后,你应该有所体会了,
xhr
和
fetch
方式是通过编程的方式控制http请求,而其他方式都是浏览器自身控制的下载请求。前者就属于先下后存的方式,可能占用较多的内存资源,而后者自带流式下载功能,适合下载安装包之类的大文件。
流式下载,边下边存
如果你不会遇到同时下载超过10个文件的情形,同时又是大文件下载,还要实时掌握下载进度,可以试试下面这种fetch流式下载方案:
asyncfunctiondownloadFile(url, filename){const response =awaitfetch(url,{method:'GET'});if(!response.ok){thrownewError(`HTTP error! status: ${response.status}`);}const contentLength = response.headers.get('Content-Length');const total =parseInt(contentLength,10);let loaded =0;const reader = response.body.getReader();const stream =newReadableStream({start(controller){functionpush(){
reader.read().then(({ done, value })=>{if(done){
controller.close();return;}
loaded += value.byteLength;
console.log(`Download progress: ${(loaded / total *100).toFixed(2)}%`);
controller.enqueue(value);push();});}push();}});const blob =awaitnewResponse(stream).blob();const downloadUrl = window.URL.createObjectURL(blob);const anchor = document.createElement('a');
anchor.href = downloadUrl;
anchor.download = filename || url.split('/').pop();
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
window.URL.revokeObjectURL(downloadUrl);
console.log('Download completed successfully');}// 调用函数开始流式下载downloadFile('https://example.com/largefile.zip','largefile.zip');
优点:
- 节省内存:通过流式处理,文件在下载过程中不会占用大量内存,特别适合大文件下载。
- 实时反馈:可以实时显示下载进度,用户体验更好。(依赖
Content-Length
响应头) - 优化性能:减少内存占用的同时提高了下载的稳定性,避免了内存溢出等问题。
注意事项:
- 浏览器兼容性:
Fetch API
和ReadableStream
在现代浏览器中支持良好,但在较旧的浏览器中可能不兼容,需要根据需求评估兼容性问题。 - 服务器支持:服务器需要支持
Range
请求,以便浏览器可以请求部分内容。
这部分相关的 MDN 文档和示例:ReadableStream - Web API | MDN
最后总结
如果同时下载文件数量不超过10个,用fetch就够了,配合流式下载,也能下载大文件。
如果同时下载文件数量可能超过10个,可以选择隐藏
<iframe>
的方案,比
windows.open
方式体验较好,我最后也是选择了该方案。
以上仅代表个人观点,如有问题欢迎讨论。
最后,我发现浏览器是提供了对下载管理器进行交互的API的,但是仅供扩展插件中使用,就很无语。。。。😡
downloads - Mozilla | MDN
相关链接:
- 前端大文件分片下载解决方案
- 前端下载超大文件的完整方案
版权归原作者 wanzheng_96 所有, 如有侵权,请联系我们删除。