背景
后端下載接口采用blob 流式下載大文件且需要鑒權(quán),前端也相應(yīng)的改用流式下載,2G 以下的測(cè)試沒(méi)問(wèn)題。
大文件下載測(cè)試
隨后測(cè)試了下大文件下載,準(zhǔn)備了一個(gè)6G 多的文件。第一個(gè)次下載可以,再次下載該文件就報(bào)錯(cuò):net::err_failed。如下圖,兩個(gè)文件大概到了10G 多就失敗了,報(bào)錯(cuò)如下:
但是,再次下載小于5G 的文件是能下載的。
前端實(shí)現(xiàn)方式如下:
const res = await $API('downloadFile', null, null, id, {
timeout: 7200 * 1000,
responseType: 'blob'
})
// 直接返回文件內(nèi)容,有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、是不是磁盤滿了?
查看虛擬機(jī)磁盤還剩30G ,排除。
2、chrome瀏覽器是不是會(huì)緩存blob,并對(duì)blob 大小有限制?
搜索了很多文章發(fā)現(xiàn),確實(shí)會(huì)緩存并且有限制。blob存儲(chǔ)在瀏覽器的沙盒文件系統(tǒng)中,當(dāng)瀏覽器下載或讀取Blob文件時(shí),會(huì)將文件存儲(chǔ)在瀏覽器的緩存中。這種緩存機(jī)制會(huì)受到內(nèi)存限制,會(huì)遇到內(nèi)存不足的問(wèn)題。所以不好判斷,但肯定和內(nèi)存有關(guān)。
所以這種寫法可能是超出瀏覽器的blob 限制或者內(nèi)存限制了。因?yàn)閍xios 是需要等待整個(gè)blob文件流返回才會(huì)結(jié)束請(qǐng)求,整個(gè)響應(yīng)是加載到瀏覽器的內(nèi)存中的,響應(yīng)結(jié)束之后前端才能構(gòu)建 blob 對(duì)象,轉(zhuǎn)化成文件下載,而不是邊下載邊保存文件的。有時(shí)也會(huì)出現(xiàn)頁(yè)面崩潰的情況。
解決辦法
由于后端不想繞過(guò)登錄解決鑒權(quán),問(wèn)題,只能從前端先想辦法。在FileSaver.js 里看到推薦下載2G 以上的文件用StreamSaver.js,搭配 fetch 可以實(shí)現(xiàn)邊下載邊保存。
1、StreamSaver.js下載原理
模擬了服務(wù)器保存文件所要做的事情:給mitm.html 頁(yè)面發(fā)送一個(gè)帶有Content-Disposition標(biāo)頭的流,告訴瀏覽器保存文件。同時(shí)創(chuàng)建一個(gè)sw.js作為服務(wù)器,由 service worker 創(chuàng)建一個(gè)下載鏈接,然后打開(kāi)這個(gè)鏈接。StreamSaver.js 在github上的2個(gè)托管文件:
- mitm.html:作為web頁(yè)面和service worker消息通信的中間人,加工處理web頁(yè)面消息以及MessageChannel給service worker;注冊(cè)管理service worker,防重啟。
- sw.js:充當(dāng)服務(wù)器,用來(lái)攔截請(qǐng)求,制造假的響應(yīng),讓瀏覽器去下載資源
它通過(guò)直接創(chuàng)建一個(gè)可寫流到文件系統(tǒng)的方法,而不是將數(shù)據(jù)保存在客戶端存儲(chǔ)或內(nèi)存中。解決了內(nèi)存占用過(guò)大的問(wèn)題。
2、代碼實(shí)現(xiàn)
export const downloadFileByStreamSaver = (url, fileName) => {
//fetch 默認(rèn)沒(méi)有超時(shí)限制
fetch(url, {
method: 'get',
headers: {
Authorization: localStorage.getItem('token'),
responseType: 'blob'
}
}).then(res => {
//如果是文件就下載,需要后端header設(shè)置Content-Disposition
if (res.headers.get('Content-Disposition')) {
// 創(chuàng)建一個(gè)文件,該文件支持寫入操作
const fileStream = streamSaver.createWriteStream(fileName)
const readableStream = res.body
// more optimized
if (window.WritableStream && readableStream.pipeTo) {
return readableStream.pipeTo(fileStream).then(() => {
ElMessage.success('下載文件成功!')
})
}
// 監(jiān)聽(tīng)文件內(nèi)容是否讀取完整,讀取完就執(zhí)行“保存并關(guān)閉文件”的操作
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 {
//不是文件應(yīng)該就是報(bào)錯(cuò)了
res.json().then(json => {
errorMessageTip({
tipMessage: json.message || '下載文件失敗',
title: '下載文件'
})
})
}
})
}
大文件下載問(wèn)題解決。
3、 缺點(diǎn)
- 需要去訪問(wèn)官方的sw.js來(lái)攔截請(qǐng)求,故而下載時(shí)會(huì)出現(xiàn)一個(gè)短暫的彈框,影響交互體驗(yàn)(官方說(shuō)明使用https 以后會(huì)沒(méi)有彈框,故下面緊接著的問(wèn)題可能也不會(huì)存在)
因?yàn)槌鲇诎踩剂浚琒ervice workers只能由HTTPS承載,因此域名不是https 的話也會(huì)報(bào)錯(cuò)。
-
彈框受到瀏覽器限制,如果用戶禁止彈框,那這個(gè)下載是會(huì)被攔截的,故而不會(huì)下載成功。
下載被攔截 為了穩(wěn)定性需要自己部署mitm.html和serviceWorker.js,和index.html 同級(jí)就行。
總結(jié)
1、后端有鑒權(quán)的下載
- blob:適合動(dòng)態(tài)生成的下載一些動(dòng)態(tài)數(shù)據(jù)或者小文件
- fetch +StreamSaver:大文件
- arraybuffer:沒(méi)試驗(yàn)過(guò),有興趣的可以讓后端試試
- base64:大文件可能也有內(nèi)存溢出問(wèn)題
2、后端無(wú)鑒權(quán),可以生成靜態(tài)url----最推薦
- a 標(biāo)簽:<a download="附件.pdf">下載文件</a>
- window.open或location.href
3、 nginx 下載限制
nginx代理的緩存默認(rèn)為1個(gè)G,可以在nginx配置proxy_max_temp_file_size等關(guān)于緩存的配置項(xiàng)
4、關(guān)于header 設(shè)置Content-disposition
Content-disposition是告訴瀏覽器文件保存在本地還是瀏覽器內(nèi)存。當(dāng)響應(yīng)類型為application/octet-stream時(shí),如果使用了Content-Disposition頭信息,那么意味著不想直接顯示內(nèi)容,而是想彈出一個(gè)“文件下載”的對(duì)話框。關(guān)鍵在于一定要加上attachment,這樣的話,瀏覽器在打開(kāi)的時(shí)候會(huì)提示保存還是打開(kāi),即使選擇打開(kāi),也會(huì)使用相關(guān)聯(lián)的程序,比如記事本打開(kāi),而不是瀏覽器直接打開(kāi)。
5、下面這種fetch寫法還是blob文件流的下載方式,還是會(huì)先下載完全部blob數(shù)據(jù)才可以保存
所以,只要是blob文件流的下載方式,都是先下載完全部數(shù)據(jù)才彈出保存窗口。
參考文章
如何用 JavaScript 下載文件
google-chrome - xhr blob responseType 的內(nèi)存使用情況(Chrome)
瀏覽器blob限制
Fetch API
Fetch API Response
ReadableStream
前端自個(gè)突破瀏覽器Blob和RAM大小限制保存文件的騷玩法!
streamsaver——下載打包2GB以上的文件
HTTP知多少——Content-disposition(文件下載)
vue前端下載阿里oss超大文件的問(wèn)題