在 WEB 開發中,我們會期望使用者在點選某個連結的時候,下載一個檔案(不管這個檔案能不能被瀏覽器解析,都要下載)。以前接觸過一種方式,就是在響應 header 中設定 force-download
:
1 2 |
Content-Type: application/force-download Content-Disposition: attachment; filename="test.zip" |
然而,這是一種 hack 方式,並不推薦使用:
Content-Type: application/force-download means “I, the web server, am going to lie to you (the browser) about what this file is so that you will not treat it as a PDF/Word Document/MP3/whatever and prompt the user to save the mysterious file to disk instead”. It is a dirty hack that breaks horribly when the client doesn’t do “save to disk”.
有位小夥伴就遇到了不奏效的情況:
ATTENTION:
If you use any of the lines below your download will probably NOT WORK on Android 2.1.Content-Type: application/force-download
Content-Disposition: attachment; filename=MyFileName.ZIP
Content-Disposition: attachment; filename=”MyFileName.zip”
Content-Disposition: attachment; filename=”MyFileName.ZIP”;
那麼,究竟怎麼辦呢?接下來描述我的同事和我遇到的問題。
問題發現
最近接手了一個新專案,今天剛好有空熟悉一下之前的功能。於是開啟線上地址,輸入測試賬號,進入一個列表頁面,這個列表頁面提供了下載資料為 Excel 檔案的功能,點了一下下載
連結,猛然發現,下載的檔名字怎麼是 download
?為啥呢?
我用的瀏覽器是 Chrome 51 ,系統是 OS EI Capitan 10.11.5 。
我一同事 Chrome 47,可以完全正常下載!
先看看為啥我的瀏覽器不行吧!
第一步探索
開啟 Chrome 開發者工具,檢視 HTTP 請求,發現響應頭部有如下兩項:
1 2 |
Content-Type: application/octet-stream;charset=GBK Content-Disposition: attachment; filename="%D6%D0%CE%C4.xlsx |
第二步探索噢,filename 那裡多了一個雙引號,去掉吧!
然而,引號去掉之後,問題依舊!什麼情況?難道是 filename 需要引號包起來?
好吧,包起來試試!
第三步探索
包起來後問題依舊,什麼鬼?
靈機一動,去看看別人怎麼做的吧!於是找到別人網站一個下載 Excel 的頁面,點選下載,發現響應 header 裡面是這樣的:
1 2 |
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8 Content-Disposition: inline;filename="%D6%D0%CE%C4.xlsx";filename*=utf-8''%D6%D0%CE%C4.xlsx |
經過一堆胡亂搜尋,猜測 utf-8 就是檔名的編碼。為啥檔名要編碼呢?呃,HTTP header 裡面還未見過中文……Content-Type 指明瞭具體的檔案型別,然後 Content-Disposition 多了一個 filename*=
,這是什麼東西? utf-8
是什麼編碼?
好了,我們後端的程式碼大致做法是這樣的:
1 2 |
response.addHeader("Content-Type", "application/octet-stream"); response.addHeader("Content-Disposition", "attachment; filename=\"" + new String(fileName.getBytes("GBK"), "ISO-8859-1") + "\".xlsx"); |
1 2 |
response.addHeader("Content-Type", "application/octet-stream"); response.addHeader("Content-Disposition", "attachment; filename=\"" + new String(fileName.getBytes("GBK"), "ISO-8859-1") + "\".xlsx;filename*=GBK''" + new String(fileName.getBytes("GBK"), "ISO-8859-1")); |
好了,我再點選下載,沒問題!看起來,只需要用 filename*=
附上編碼就行了,於是後端程式碼改成:
第四步探索
看起來好像是 OK 了,但是,用 IE 試一下,又不正常了,檔名字不對了!
為什麼呢?別人網站在 IE 下都能正常下載的!現在主要有兩處區別:
- 我們的 Content-Type 沒有寫具體;
- 我們使用了 GBK 編碼。
一思索,感覺編碼的嫌疑較大,為啥呢?因為對於檔案下載,瀏覽器根本不用管檔案內容是個啥,只需要按照二進位制流寫入本地磁碟就好了,並且,此處也只是檔名錯了,下載下來的檔案內容還是沒問題的。
那就改編碼吧,改成 UTF-8 :
1 2 |
response.addHeader("Content-Type", "application/octet-stream"); response.addHeader("Content-Disposition", "attachment; filename=\"" + new String(fileName.getBytes("UTF-8"), "ISO-8859-1") + "\".xlsx;filename*=UTF-8''" + new String(fileName.getBytes("UTF-8"), "ISO-8859-1")); |
總結經測試,一切正常!
在檔案下載功能中,一般都會藉助於這兩個 header 來達到效果,那麼兩個 header 的具體作用是什麼呢?
- Content-Type:告訴瀏覽器當前的響應體是個什麼型別的資料。當其為 application/octet-stream 的時候,就說明 body 裡面是一堆不知道是啥的二進位制資料。
- Content-Disposition:用於向瀏覽器提供一些關於如何處理響應內容的額外的資訊,同時也可以附帶一些其它資料,比如在儲存響應體到本地的時候應該使用什麼樣的檔名。
細想一下, Content-Type 好像對於檔案下載沒什麼作用?事實上的確如此。可是再想一下,如果瀏覽器不理會 Content-Disposition ,不下載檔案怎麼辦?如果此時提供了 Content-Type ,至少瀏覽器還有機會根據具體的 Content-Type 對響應體進行處理。
可是為什麼瀏覽器會不理會 Content-Disposition 呢?因為這個 Content-Disposition 頭部並不是 HTTP 標準中的內容,只是被瀏覽器廣泛實現的一個 header 而已。
話題轉一轉, Content-Disposition 的語法見此處,其中相對重要的點此處羅列一下:
- 常用的 disponsition-type 有
inline
和attachment
:- inline:建議瀏覽器使用預設的行為處理響應體。
- attachment:建議瀏覽器將響應體儲存到本地,而不是正常處理響應體。
- Content-Disposition 中可以傳入 filename 引數,有兩種形式:
- filename=yourfilename.suffix:直接指明檔名和字尾。
- filename*=utf-8’’yourfilename.suffix:指定了檔名編碼。其中,編碼後面那對單引號中還可以填入內容,此處不贅述,可參考規範。
- 有些瀏覽器不認識
filename*=utf-8''yourfilename.suffix
(估計因為這東西比較複雜),所以最好帶上filename=yourfilename.suffix
。