背景
后端下载接口采用blob 流式下载大文件且需要鉴权,前端也相应的改用流式下载,2G 以下的测试没问题。
大文件下载测试
随后测试了下大文件下载,准备了一个6G 多的文件。第一个次下载可以,再次下载该文件就报错:net::err_failed。如下图,两个文件大概到了10G 多就失败了,报错如下:
但是,再次下载小于5G 的文件是能下载的。
前端实现方式如下:
const res = await $API('downloadFile', null, null, id, {
timeout: 7200 * 1000,
responseType: 'blob'
})
// 直接返回文件内容,有code码表示失败
if (res.code) {
errorMessageTip({
tipMessage: res.message || '下载文件失败',
title: '下载文件'
})
} else {
downloadFile(name, res, true)
ElMessage.success('下载文件成功!')
}
export const downloadFile = (fileName, content) => {
const fileNames = fileName.split('.')
const fileType = fileNames[fileNames.length - 1]
let b = new Blob([content])
let link = document.createElement('a')
const url = URL.createObjectURL(b)
link.href = url
link.download = fileName
link.style.display = 'none'
document.body.appendChild(link)
link.click()
link.remove()
URL.revokeObjectURL(url)
}
原因分析
1、是不是磁盘满了?
查看虚拟机磁盘还剩30G ,排除。
2、chrome浏览器是不是会缓存blob,并对blob 大小有限制?
搜索了很多文章发现,确实会缓存并且有限制。blob存储在浏览器的沙盒文件系统中,当浏览器下载或读取Blob文件时,会将文件存储在浏览器的缓存中。这种缓存机制会受到内存限制,会遇到内存不足的问题。所以不好判断,但肯定和内存有关。
所以这种写法可能是超出浏览器的blob 限制或者内存限制了。因为axios 是需要等待整个blob文件流返回才会结束请求,整个响应是加载到浏览器的内存中的,响应结束之后前端才能构建 blob 对象,转化成文件下载,而不是边下载边保存文件的。有时也会出现页面崩溃的情况。
解决办法
由于后端不想绕过登录解决鉴权,问题,只能从前端先想办法。在FileSaver.js 里看到推荐下载2G 以上的文件用StreamSaver.js,搭配 fetch 可以实现边下载边保存。
1、StreamSaver.js下载原理
模拟了服务器保存文件所要做的事情:给mitm.html 页面发送一个带有Content-Disposition标头的流,告诉浏览器保存文件。同时创建一个sw.js作为服务器,由 service worker 创建一个下载链接,然后打开这个链接。StreamSaver.js 在github上的2个托管文件:
- mitm.html:作为web页面和service worker消息通信的中间人,加工处理web页面消息以及MessageChannel给service worker;注册管理service worker,防重启。
- sw.js:充当服务器,用来拦截请求,制造假的响应,让浏览器去下载资源
它通过直接创建一个可写流到文件系统的方法,而不是将数据保存在客户端存储或内存中。解决了内存占用过大的问题。
2、代码实现
export const downloadFileByStreamSaver = (url, fileName) => {
//fetch 默认没有超时限制
fetch(url, {
method: 'get',
headers: {
Authorization: localStorage.getItem('token'),
responseType: 'blob'
}
}).then(res => {
//如果是文件就下载,需要后端header设置Content-Disposition
if (res.headers.get('Content-Disposition')) {
// 创建一个文件,该文件支持写入操作
const fileStream = streamSaver.createWriteStream(fileName)
const readableStream = res.body
// more optimized
if (window.WritableStream && readableStream.pipeTo) {
return readableStream.pipeTo(fileStream).then(() => {
ElMessage.success('下载文件成功!')
})
}
// 监听文件内容是否读取完整,读取完就执行“保存并关闭文件”的操作
const writer = fileStream.getWriter()
const reader = res.body.getReader()
const pump = () =>
reader.read().then(res => {
if (res.done) {
writer.close()
} else {
writer.write(res.value).then(pump)
}
})
pump()
} else {
//不是文件应该就是报错了
res.json().then(json => {
errorMessageTip({
tipMessage: json.message || '下载文件失败',
title: '下载文件'
})
})
}
})
}
大文件下载问题解决。
3、 缺点
- 需要去访问官方的sw.js来拦截请求,故而下载时会出现一个短暂的弹框,影响交互体验(官方说明使用https 以后会没有弹框,故下面紧接着的问题可能也不会存在)
因为出于安全考量,Service workers只能由HTTPS承载,因此域名不是https 的话也会报错。
-
弹框受到浏览器限制,如果用户禁止弹框,那这个下载是会被拦截的,故而不会下载成功。
为了稳定性需要自己部署mitm.html和serviceWorker.js,和index.html 同级就行。
总结
1、后端有鉴权的下载
- blob:适合动态生成的下载一些动态数据或者小文件
- fetch +StreamSaver:大文件
- arraybuffer:没试验过,有兴趣的可以让后端试试
- base64:大文件可能也有内存溢出问题
2、后端无鉴权,可以生成静态url----最推荐
- a 标签:<a href="https://www.baidu.top.pdf" download="附件.pdf">下载文件</a>
- window.open或location.href
3、 nginx 下载限制
nginx代理的缓存默认为1个G,可以在nginx配置proxy_max_temp_file_size等关于缓存的配置项
4、关于header 设置Content-disposition
Content-disposition是告诉浏览器文件保存在本地还是浏览器内存。当响应类型为application/octet-stream时,如果使用了Content-Disposition头信息,那么意味着不想直接显示内容,而是想弹出一个“文件下载”的对话框。关键在于一定要加上attachment,这样的话,浏览器在打开的时候会提示保存还是打开,即使选择打开,也会使用相关联的程序,比如记事本打开,而不是浏览器直接打开。
5、下面这种fetch写法还是blob文件流的下载方式,还是会先下载完全部blob数据才可以保存
所以,只要是blob文件流的下载方式,都是先下载完全部数据才弹出保存窗口。
参考文章
如何用 JavaScript 下载文件
google-chrome - xhr blob responseType 的内存使用情况(Chrome)
浏览器blob限制
Fetch API
Fetch API Response
ReadableStream
前端自个突破浏览器Blob和RAM大小限制保存文件的骚玩法!
streamsaver——下载打包2GB以上的文件
HTTP知多少——Content-disposition(文件下载)
vue前端下载阿里oss超大文件的问题