背景
最近遇到一個下載的需求,由於 url 參數太長(常用的下載方法 a 標簽或者 location.href 的方法都是 get 請求,get 請求參數長度有限制),無法下載,考慮了好幾種方案,最終還是覺得通過 ajax 的 POST 方法進行下載,比較容易實現,下面記錄實現過程以及遇到的問題。
但是由於 AJAX 並不會喚起瀏覽器的下載窗口, AJAX 設計的初衷就是用來實現 異步刷新 的,用以改善原始的 form 表單提交刷新頁面的問題,那么如何來解決呢?
POST 方法下載實現原理
通過 fetch 請求獲取文件,然后利用 Blob 對象來接收處理,在接收到后端返回的文件后,把其轉化一下,放入 a標簽 的 href 中,並觸發下載行為。
實現的代碼如下:
fetch(url, {
method: 'POST', body: jsON.stringify(params), header: { 'Content-Type': 'application/json;charset=UTF-8' } }).then(function(response) { return response.blob(); }).then(function(blob) { const link = document.createElement('a') link.style.display = 'none' link.href = URL.createObjectURL(blob) document.body.appendChild(link) link.click() // 釋放的 URL 對象以及移除 a 標簽 URL.revokeObjectURL(link.href) document.body.removeChild(link) });
這里需要注意的是要記得要調用 response 的 blob 方法,這樣才會返回一個 blob,如果你沒用過 blob 的話,可能你以前只知道 json 和 text,其實 response 的 body 還可以轉化為 arrayBuffer 和 formData 。
如何拿到文件名
可以下載文件了只是第一步,但是你會發現還有一個問題,下載下來的文件名是你看不懂的名字,類似這樣:
我這邊的方案是把文件名放在 response 的 headers 里,放在 content-disposition 字段里,有個 fileName 字段,用來存放文件名。
我感覺在下載文件的時候 content-disposition 字段對於他們后端來說感覺是都會加的,因為最開始我用 get 下載的時候就已經有這個字段了,如果你們后端沒有設置這個 header ,可以設置一下,當然也可以設置到其他字段里。
一個小插曲
當我把 fetch 后的 res 打印出來看 Response 的時候,發現 headers 里是空對象,如下:
然后我再通過 res.headers 直接去拿 headers,發現還是一個 Headers 的空對象。
header{}
我還以為 headers 里面沒有東西,但是當我直接通過 res.headers.get('content-disposition') 去拿的時候,竟然拿到了,數據像這樣:
attachment;fileName=%E7%9B%B4%E6%92%AD%E6%97%B6%E9%95%BF%E4%B8%BB%E6%92%AD%E6%98%8E%E7%BB%86.xls
然后你就可以通過多種方式將文件名給提取出來,我這里采用的是通過 split 方法來提取的。
res.headers.get('content-disposition').split(';')[1].split('=')[1]
資源搜索網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com
最終的實現
准備工作都做好了,然后就寫出了這樣的代碼:
fetch(url, {
method: 'POST', body: JSON.stringify(params), header: { 'Content-Type': 'application/json;charset=UTF-8' } }).then(function(response) { const filename = res.headers.get('content-disposition').split(';')[1].split('=')[1] return { filename, blob: response.blob() } }).then(function(obj) { const link = document.createElement('a') link.style.display = 'none' // a 標簽的 download 屬性就是下載下來的文件名 link.download = obj.filename link.href = URL.createObjectURL(obj.blob) document.body.appendChild(link) link.click() // 釋放的 URL 對象以及移除 a 標簽 URL.revokeObjectURL(link.href) document.body.removeChild(link) });
本以為就可以了,但是下載下來打開 excel 發現內容是 Promise,然后才發現原來 response.blob() 返回的是一個 promise。
所以改進的實現方案如下:
fetch(url, {
method: 'POST', body: JSON.stringify(params), header: { 'Content-Type': 'application/json;charset=UTF-8' } }).then(function(response) { const filename = res.headers.get('content-disposition').split(';')[1].split('=')[1] response.blob().then(blob => { const link = document.createElement('a') link.style.display = 'none' // a 標簽的 download 屬性就是下載下來的文件名 link.download = filename link.href = URL.createObjectURL(blob) document.body.appendChild(link) link.click() // 釋放的 URL 對象以及移除 a 標簽 URL.revokeObjectURL(link.href) document.body.removeChild(link) }) })
不過這種 then 里面又套了 then ,看着有點不好看,所以用 async/await 重新寫了一版:
async function postDownload(url, params) { const request = { body: JSON.stringify(params), method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8' } } const response = await fetch(url, request) const filename = response.headers.get('content-disposition').split(';')[1].split('=')[1] const blob = await response.blob() const link = document.createElement('a') link.download = decodeURIComponent(filename) link.style.display = 'none' link.href = URL.createObjectURL(blob) document.body.appendChild(link) link.click() URL.revokeObjectURL(link.href) document.body.removeChild(link)j }
這個函數里沒有寫任何的錯誤處理,那也不是這篇文章要講的,不過自己在實現的時候應該加上 try/catch,不然如果有問題,不報錯還是很難受的。