這篇文章聊聊跟前端文件下載相關的一些知識。javascript
說到前端下載文件,我最早想到的是在學校的時候,本身搭建 nginx + php 環境,以後打開頁面 http://localhost:80/index.php
, 卻奇怪的發現,每次打開都會變成文件下載。php
後來我才知道,請求頭裏面會有 Accept
字段,響應頭裏面會有 Content-Type
字段,前者用來告訴 S
端能接受哪些類型的內容,後者告訴 C
端返回來的又是什麼類型的內容。html
MIME 是一種標準化的方式來表示文檔的性質和格式,瀏覽器一般使用 MIME 來肯定類型(而不是文件擴展名)。前端
content-type 使用的都是 MIME 類型,jpg 文件對應 image/jpeg
, js 文件對應 application/javascript
,xlsx 則是 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
。html5
MIME 有兩種默認類型:java
text/plain
表示文本文件的默認值。一個文本文件應當是人類可讀的,而且不包含二進制數據。application/octet-stream
表示全部其餘狀況的默認值。一種未知的文件類型應當使用此類型。完整的 MIME 類型列表nginx
👆index.php
會變成文件下載的緣由是我因爲安裝錯誤,沒有正確解析 php 文件,nginx 直接訪問到文件,並加上默認 contentType application/octet-stream
。由於 Chrome 不能執行 application/octet-stream
格式的文件,默認操做是把它下載下來,(不一樣瀏覽器對待不能處理的文件執行的操做不同,有些瀏覽器則會嘗試去嗅探)。git
這也能解釋爲何咱們直接訪問https://xxx/foo/bar.zip
等資源的時候,瀏覽器會直接下載。github
當服務端返回瀏覽器不支持的 MIME 類型,部分瀏覽器會嘗試去嗅探它,幫大意的開發者修正這一錯誤,但這可能會致使你的網站遭受攻擊。比方說,用戶上傳一張大熊貓圖片,內容以下:json
其實是個 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
👹 ,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',
};
});
複製代碼
而後訪問剛纔的路由,就能看到文件下載下來了。
還有一種方式讓瀏覽器把文件保存到本地。就是 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 處理。
這篇文章本來標題叫《宇宙最強前端拖拽上傳和文件下載》,寫到一半查資料的時候發現掘金已經有不少人寫過相似的文章。
心態崩了,改稿已經來不及,就這樣吧。(浪費了大半天時間) 祝你們春節快樂,年終獎紅紅火火。