1.1 a 标签下载
只要在浏览器能通过 url 访问到文件,那就能通过 a 标签下载,不需要任何服务器配置
问题是:图片能直接下载,但如果是 pdf,浏览器默认会打开 pdf,而不是下载(即使设置 download)
怎么解决这个问题?
一、使用 blob 流强制下载
- 优点:可以自定义文件名
- 缺点:需要服务器配置跨域('Access-Control-Allow-Origin')
二、后端响应头指定文件类型 Content-Type 和文件名 Content-Disposition
/**
* 通用文件下载(可自定义文件名,需要服务器支持跨域)
* 注意:大文件blob转换可能内存溢出
* @param {string} url 文件地址
* @param {string} fileName 自定义文件名
* @param {boolean} supportCORS 服务器是否支持跨域
*/
function downloadFile(url, fileName, supportCORS = true) {
if (supportCORS) {
// 保证强制下载,可自定义文件名
let x = new window.XMLHttpRequest();
let timestamp = `t=${new Date().getTime()}`;
let separator = url.includes('?') ? '&' : '?';
x.open('GET', `${url}${separator}${timestamp}`);
x.responseType = 'blob';
x.onload = () => {
let url = window.URL.createObjectURL(x.response);
let a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
a.remove();
window.URL.revokeObjectURL(url);
};
x.send();
} else {
// 如果是pdf,浏览器默认会打开而不是下载
let a = document.createElement('a');
a.href = url;
a.download = fileName;
a.target = '_blank';
a.click();
a.remove();
}
}
1.2 下载库
常用:FileSaver.js、StreamSaver.js
两者都用于文件下载,主要区别在于,是否将整个文件加载到内存中
FileSaver.js 原理:
- 将文件数据封装成为 Blob 对象
- 然后利用 URL.createObjectURL(blob) 创建临时文件链接
- 通过 a 标签的 download 属性触发浏览器的下载行为
StreamSaver.js 原理(比较复杂)
- 注册 Service Worker(SW)
- fetch 发起带自定义 headers 的流式请求(Accept: application/octet-stream)
- SW 拦截请求并伪造响应头(Content-Disposition: attachment,Content-Type: application/octet-stream),强制激活浏览器下载
- 最后利用 WritableStream API 将返回的文件流(ReadableStream)直接写入文件系统
1.3 纯前端批量下载方案
方案一:使用 a 标签循环下载
- 优点:实现最简单
- 缺点:需要用户点击允许下载多文件、反复弹窗用户体验差
方案二:jszip + FileSaver.js,先压缩再下载
-
优点:比方案一用户体验好
-
缺点:
- 压缩需要时间,等压缩完才会调出浏览器下载弹窗 》可以增加一个进度条
- FileSaver 下载文件总大小不能超过 2GB
方案二:StreamSaver.js + zip-stream.js,先压缩再流式下载
- 优点:将下载逻辑移至 Web Worker,避免阻塞主线程,并且会直接调出浏览器下载弹窗
- 缺点:zip-stream 打包文件总大小不能超过 4GB
综上,纯前端实现批量下载,文件总大小不能超过 4GB
如果需要下载更大的文件,必须后端先把文件下载到服务器,然后把文件拆分成小块,再由前端下载
handleBatchDownload: () => {
// 获取所有选中文件
const selectArr = ownFileList.value.filter((item) => orderAttachment.selectedRowKeys.includes(item.id))
// 如果所有选中文件的文件大小之和超过4GB,则提示用户
const totalSize = selectArr.reduce((acc, cur) => acc + cur.size, 0)
if (totalSize > 4 * 1024 * 1024 * 1024) {
Modal.error({
title: '附件过大',
content: '文件总大小超过4G,无法批量下载,请分批下载',
centered: true,
zIndex: 9999,
})
return
}
// 注意:无法生成超过 4GB 的 zip 文件
const readableZipStream = new window.ZIP({
async pull(ctrl) {
for (const item of selectArr) {
try {
const res = await fetch(item.url)
if (!res.ok) {
console.error(`fetch error: ${item.url}`)
continue
}
const stream = () => res.body
const name = item.name.split('/').pop()
ctrl.enqueue({ name, stream })
} catch (error) {
console.error(`unknown error: ${item.url}`, error)
continue
}
}
ctrl.close()
},
})
const fileStream = streamSaver.createWriteStream('附件汇总.zip')
if (window.WritableStream && readableZipStream.pipeTo) {
readableZipStream
.pipeTo(fileStream)
.then(() => {
console.log('下载成功')
})
.catch((error) => {
console.error('下载失败', error)
})
} else {
// 兼容旧浏览器
const writer = fileStream.getWriter()
const reader = readableZipStream.getReader()
const pump = () =>
reader.read().then((res) => (res.done ? writer.close() : writer.write(res.value).then(pump)))
pump()
}
},
1.4 最后
如果帮到你了可以点个赞。
2025/06/07:发布文章