文件下載,竟難住了我們CTO...
今天給大家分享兩個(gè)比較有用的瀏覽器行為與預(yù)期不一致的現(xiàn)象,這兩個(gè)問(wèn)題其實(shí)并不是什么難題,但在工作中發(fā)現(xiàn)不少人被難住了。
圖片來(lái)自包圖網(wǎng)
在我的印象中至少有三位同事在群里問(wèn)這樣的問(wèn)題,上周又有同事被此現(xiàn)象困住了,所以我覺得這應(yīng)該是個(gè)共性問(wèn)題,在這里分享給大家,希望對(duì)大家有幫助。
現(xiàn)象一:點(diǎn)擊按鈕無(wú)法實(shí)現(xiàn)文件下載
前端同事反饋在瀏覽器里點(diǎn)擊實(shí)現(xiàn)好的「下載商品圖片」按鈕卻無(wú)法下載。(預(yù)期應(yīng)該下載 zip 文件)
但如果你在瀏覽器的地址欄里輸入此下載地址卻又能直接從瀏覽器里下載,這是為何?
我們可以打開調(diào)試工具「網(wǎng)絡(luò)部分」,然后點(diǎn)擊一下上面的「下載商品圖片」,首先看一下網(wǎng)絡(luò)請(qǐng)求是否正常。
①首先看請(qǐng)求頭,可以看出狀態(tài)碼是 200,另外還有 content-disposition 與 Content-Type 這兩個(gè) response header:
畫外音:Content-Type: application/octet-stream 告訴客戶端這是一個(gè)二進(jìn)制文件,content-disposition 告訴客戶端這是一個(gè)需要下載的附件并告訴瀏覽器該附件默認(rèn)的文件名。
②再看此請(qǐng)求的 response body,是否和步驟一的 application/octet-stream 相符:
可以看到 response 就是一堆亂碼,即文件的二進(jìn)制流表現(xiàn)形式,所以從請(qǐng)求來(lái)看其實(shí)是沒有問(wèn)題的,文件是正常的返回的,但為啥文件卻沒有下載下來(lái)。
下載下來(lái)的文件去哪里了呢,注意看上圖的另一個(gè)紅框 XHR ,它的全稱是 XMLHttpRequest,是 ajax 請(qǐng)求的一種表現(xiàn)形式。
ajax 本身無(wú)法觸發(fā)瀏覽器的下載功能, 它的 response 會(huì)交由 JavaScript 處理,使用 ajax 下載完成后,response 以字符串的形式存儲(chǔ)在內(nèi)存中,那使用 ajax 就沒法下載了嗎?不是的,我們看下瀏覽器為啥能下載。
我們發(fā)現(xiàn)使用瀏覽器的 GET 請(qǐng)求(主要以 frame 加載,a 標(biāo)簽點(diǎn)擊觸發(fā))或 POST 請(qǐng)求(以 form 的形式存在)是可以下載文件的。
因?yàn)檫@是瀏覽器的內(nèi)置事件,下載的 response 會(huì)交由瀏覽器自己處理,瀏覽器如果識(shí)別到是二進(jìn)制流數(shù)據(jù)則下載,如果識(shí)別到是可以打開的文件,如 xml,image 等則不會(huì)下載,會(huì)以預(yù)覽的樣式存在。
那么為啥 ajax 不能默認(rèn)實(shí)現(xiàn)文件下載呢,這是瀏覽器的安全策略限制的,試想如果 ajax 可以下載文件,那就意味著 ajax 可以直接與磁盤交互,這會(huì)存在嚴(yán)重的安全隱患。
根據(jù)以上分析,要使用 ajax 下載文件我們也就有思路了,既然使用 a 標(biāo)簽(或 frame)的點(diǎn)擊事件可以觸發(fā)瀏覽器的內(nèi)置下載行為。
那我們?cè)谟?ajax 下載拿到 response 后,可以用 js 新建一個(gè)隱藏的 a 標(biāo)簽(標(biāo)簽的 href 指向文件的鏈接),執(zhí)行它的 click 事件。
這樣就觸發(fā)了瀏覽器的內(nèi)置下載事件,就可以下載文件了,不過(guò)需要注意的事,創(chuàng)建的 a 標(biāo)簽中要添加一個(gè) download 屬性。
這個(gè) download 屬性有啥用呢,對(duì)于瀏覽器能打開的文件,例如 html,xml 等,如果你不加 download,點(diǎn)擊 a 標(biāo)簽就不是下載了,而是打開。(注意 download 屬性目前只被火狐和谷歌兼容)
使用 ajax 來(lái)執(zhí)行下載文件的代碼示例如下:
- const filename = response.headers['content-disposition'].match(
- /filename=(.*)/
- )[1]
- // 首先要?jiǎng)?chuàng)建一個(gè) Blob 對(duì)象(表示不可變、原始數(shù)據(jù)的類文件對(duì)象)
- const blob = new Blob([response.data], {type: 'application/zip'});
- if (typeof window.navigator.msSaveBlob !== 'undefined') {
- // 兼容IE,window.navigator.msSaveBlob:以本地方式保存文件
- window.navigator.msSaveBlob(blob, decodeURI(filename))
- } else {
- let elink = document.createElement("a"); // 創(chuàng)建一個(gè)<a>標(biāo)簽
- elink.style.display = "none"; // 隱藏標(biāo)簽
- elink.href = window.URL.createObjectURL(blob); // 配置href,指向本地文件的內(nèi)存地址
- elink.download = filename;
- elink.click();
- URL.revokeObjectURL(elink.href); // 釋放URL 對(duì)象
- document.body.removeChild(elink); // 移除<a>標(biāo)簽
- }
現(xiàn)象二:在瀏覽器輸入圖片鏈接想預(yù)覽,結(jié)果卻變成了下載圖片
這個(gè)問(wèn)題其實(shí)經(jīng)由上文分析,相信你不難猜出是咋回事,我們先抓包看一下:
可以看到返回的 Content-Type 為 octet-stream,上文我們提到,它指任意類型的二進(jìn)制流數(shù)據(jù),一般下載文件返回的是這種類型,瀏覽器由于無(wú)法識(shí)別打開流數(shù)據(jù),所以會(huì)下載。
那為啥大多數(shù)圖片在瀏覽器上是可以預(yù)覽的呢,因?yàn)樗祷氐?Content-Type 是 image/png 或 image/jpeg 等瀏覽器可以直接識(shí)別打開的文件,這樣就不會(huì)執(zhí)行下載事件。
總結(jié)
以上兩個(gè)問(wèn)題需要我們對(duì)瀏覽器的工作機(jī)制與 HTTP 協(xié)議有一定的了解,所以基礎(chǔ)真的很重要啊,不然很可能你排查半天也無(wú)從下手,但如果你知道了這些原理,抓個(gè)包分析一下它們的 Content-Type,瞬間就豁然開朗了!
另外對(duì)一些疑難雜癥,了解 HTTP 協(xié)議與瀏覽器的工作機(jī)制也有助于幫助你快速定位解決問(wèn)題。
比如上圖的解決方案中我們通過(guò) content-disposition 來(lái)獲取文件的名稱:
- const filename = response.headers['content-disposition'].match(
- /filename=(.*)/
- )[1]
但在最開始發(fā)現(xiàn)這段代碼有問(wèn)題,打印日志發(fā)現(xiàn) response.headers['content-disposition'] 居然為空。
可是打開瀏覽器的 network 會(huì)發(fā)現(xiàn), content-disposition 明明存在啊:
那為啥在 reponse 的 header 里拿不到 content-disposition 呢?
一查發(fā)現(xiàn)原來(lái)還是 HTTP 協(xié)議的問(wèn)題,默認(rèn)情況下,header 只有七種 simple response headers (簡(jiǎn)單響應(yīng)首部)可以暴露給外部:
- Cache-Control
- Content-Language
- Content-Length
- Content-Type
- Expires
- Last-Modified
- Pragma
這里的暴露給外部,意思是讓客戶端(比如 Chrome)可以訪問(wèn)得到,既可以在 Network 里看到,也可以在代碼里獲取到他們的值。
而 content-disposition 不在其中,所以即使服務(wù)器在協(xié)議回包里加了該字段,如下:
- response.setHeader("content-disposition", "attachment; filename=" + filename);
但因沒“暴露”給外部,客戶端就「看得到,吃不到」。而響應(yīng)首部 Access-Control-Expose-Headers 就是控制“暴露”的開關(guān),它列出了哪些首部可以作為響應(yīng)的一部分暴露給外部。
所以如果想要讓客戶端可以訪問(wèn)到其他的首部信息,服務(wù)器不僅要在 header 里加入該首部,還要將它們?cè)? Access-Control-Expose-Headers 里面列出來(lái),如下:
- response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
- response.setHeader("content-disposition", "attachment; filename=" + filename);
這樣的話 JS 的 response header 里就有 content-disposition 的值啦。
參考鏈接:
- Access-Control-Expose-Headers: http://1il58.cn/AptUz
作者:坤哥,前獨(dú)角獸技術(shù)專家,現(xiàn)創(chuàng)業(yè)者
編輯:陶家龍
出處:碼海(ID:seaofcode)