聊聊前端檔案下載
這篇文章聊聊跟前端檔案下載相關的一些知識。
說到前端下載檔案,我最先想到的是在學校的時候,自己搭建 nginx + php 環境,之後開啟頁面 http://localhost:80/index.php
, 卻奇怪的發現,每次開啟都會變成檔案下載。

後來我才知道,請求頭裡面會有 Accept
欄位,響應頭裡面會有 Content-Type
欄位,前者用來告訴 S
端能接受哪些型別的內容,後者告訴 C
端返回來的又是什麼型別的內容。
MIME
MIME 是一種標準化的方式來表示文件的性質和格式,瀏覽器通常使用 MIME 來確定型別(而不是副檔名)。
content-type 使用的都是 MIME 型別,jpg 檔案對應 image/jpeg
, js 檔案對應 application/javascript
,xlsx 則是 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
。
MIME 有兩種預設型別:
text/plain application/octet-stream
:point_up_2: index.php
會變成檔案下載的原因是我由於安裝錯誤,沒有正確解析 php 檔案,nginx 直接訪問到檔案,並加上預設 contentType application/octet-stream
。因為 Chrome 不能執行 application/octet-stream
格式的檔案,預設操作是把它下載下來,(不同瀏覽器對待不能處理的檔案執行的操作不一樣,有些瀏覽器則會嘗試去嗅探)。
這也能解釋為什麼我們直接訪問 https://xxx/foo/bar.zip
等資源的時候,瀏覽器會直接下載。
插播安全小課堂:
當服務端返回瀏覽器不支援的 MIME 型別,部分瀏覽器會嘗試去嗅探它,幫大意的開發者修正這一錯誤,但這可能會導致你的網站遭受攻擊。比方說,使用者上傳一張大熊貓圖片,內容如下:

實際上是個 html 檔案,但是字尾名寫成 jpeg 上傳。這時候服務端如果沒有設定 contentType 直接讀取檔案返回給前端。
# koa router 演示程式碼 router.get('/assets/:file.jpeg', (ctx) => { ctx.body = fs.createReadStream(`./public/assets/${ctx.params.file}.jpeg`); }); 複製程式碼
好心的瀏覽器拿到 MIME type 為 application/octet-stream,再讀取內容發現,誒,這是個 html 啊,我們應該展現直接展示出來。

使用者看到可愛的大熊貓同時,順便把個人資訊也告訴了黑客。
為了避免發生這種安全事故,設定
X-Content-Type-Options: nosniff
router.get('/assets/:file.jpeg', (ctx) => { ctx.type = 'image/jpeg'; ctx.set('X-Content-Type-Options', 'nosniff'); ctx.body = fs.createReadStream(`./public/assets/${ctx.params.file}.jpeg`); }); 複製程式碼
僅作為演示用,koa 提供靜態資源服務應該用 koa-static
等開源包,它們會自動加上 contentType。
如何讓瀏覽器下載圖片
上面說了對應瀏覽器不支援的文件型別,預設會下載。那對於能處理的那些型別呢?比如圖片,js,json 等內容呢?
以 json 為例,由於瀏覽器知道怎麼解析,會在頁面上打印出 json 的內容。

如果需求就是讓使用者下載 json 檔案怎麼辦呢?
有另外一個響應頭部欄位 Conten-disposition
:japanese_ogre: ,Content-Disposition 指定響應的內容該以哪種形式展示,是以內聯的形式(即網頁或者頁面的一部分),還是以附件的形式下載並儲存到本地,分別對應 inline
和 attachment
。
Content-Disposition: inline Content-Disposition: attachment 複製程式碼
attachment 模式,還可以指定下載檔案的檔名和副檔名。
Content-Disposition: attachment; filename="filename.jpg" 複製程式碼
示例程式碼:
router.get('/hello.json', (ctx) => { ctx.type = 'application/json'; ctx.set('Content-Disposition', 'attachment; filename="hello.json"'); // 上面兩行程式碼,可以簡寫成 ctx.attachment('hello.json'); ctx.body = { hello: 'world', }; }); 複製程式碼
然後訪問剛才的路由,就能看到檔案下載下來了。

HTML Download 屬性
還有一種方式讓瀏覽器把檔案儲存到本地。就是 html5 a 標籤增加的 download
屬性。
<a href="/images/xxx.jpg" download="panda.jpg" >My Panda</a> 複製程式碼
當用戶點選標籤時會去下載 href 指定的檔案,並且 download
屬性的 value 對應的就是下載檔案的名字。更靈活地方式是封裝成方法,動態建立 link,觸發 click 直接下載並另存為。
<script> function downloadAs (url, fileName) { const link = document.createElement('a'); link.href = url; link.download = fileName; link.target = '_blank' document.body.appendChild(link); link.click(); link.remove(); } downloadAs('http://localhost:3001/hello.json', 'world.json'); </script> 複製程式碼
發起非同步獲取資源再下載
還有些場景,只能通過非同步請求返回二進位制內容再由前端下載。
藉助 download 屬性,結合 Blob, Url.createObjectURL() 可以實現前端非同步請求資源並匯出檔案。
const xhr = new XMLHttpRequest(); xhr.open('GET', 'http://localhost:3001/pack.zip'); xhr.responseType = 'blob'; xhr.onload = function () { const blob = xhr.response; const url = URL.createObjectURL(blob); downloadAs(url, 'mypack.zip'); URL.revokeObjectURL(url); }; xhr.send(); 複製程式碼
設定 xhr.responseType = 'blob'
那麼請求正常完成時 xhr.response
得到的就是 Blob 物件,URL.createObjectURL(Blob),得到一個 blob 的連結,形如: blob:http://localhost:3001/11a01a60-e10c-4515-825f-fb4a4219b33b
。然後就能直接當成普通 url 給 a 標籤設定 href。

Blob 物件表示一個不可變、原始資料的類檔案物件。File 物件也是基於它擴充套件的,暫時理解為抽象的檔案物件。
通過 URL.createObjectURL 會建立一個連結到 Blob 或 File 物件的 URL。這個 URL 的生命週期跟視窗繫結,避免記憶體洩漏用完應該呼叫 URL.revokeObjectURL()
釋放。
Blob 可以接受的 Javascript 原生型別資料作為引數,比方說純前端造 mock 資料,並匯出成 csv 檔案。
const rows = [ ["id", "firstname", "lastname"], ["1", "foo", "foo"], ["2", "bar", "baz"], ]; const data = rows.reduce(function(cur, next) { return cur + next.join(',') + '\n'; }, ''); const blob = new Blob([data]); const url = URL.createObjectURL(blob); downloadAs(url, 'mock.csv'); 複製程式碼
相容性
download 屬性的相容性並不高,目前只有只有 80%。可以直接使用 FileSaver.js 做了 fallback 處理。

擴充套件閱讀
吐槽
這篇文章原本標題叫《宇宙最強前端拖拽上傳和檔案下載》,寫到一半查資料的時候發現掘金已經有很多人寫過類似的文章。
心態崩了,改稿已經來不及,就這樣吧。(浪費了大半天時間)