前端文件下載和瀏覽器自動嗅探

這篇文章聊聊跟前端文件下載相關的一些知識。javascript

說到前端下載文件,我最早想到的是在學校的時候,本身搭建 nginx + php 環境,以後打開頁面 http://localhost:80/index.php, 卻奇怪的發現,每次打開都會變成文件下載。php

index.php

後來我才知道,請求頭裏面會有 Accept 字段,響應頭裏面會有 Content-Type 字段,前者用來告訴 S 端能接受哪些類型的內容,後者告訴 C 端返回來的又是什麼類型的內容。html

MIME

MIME 是一種標準化的方式來表示文檔的性質和格式,瀏覽器一般使用 MIME 來肯定類型(而不是文件擴展名)。前端

content-type 使用的都是 MIME 類型,jpg 文件對應 image/jpeg , js 文件對應 application/javascript,xlsx 則是 application/vnd.openxmlformats-officedocument.spreadsheetml.sheethtml5

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

evil

其實是個 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 啊,咱們應該展示直接展現出來。🌚🌚🌚

evil

用戶看到可愛的大熊貓同時,順便把我的信息也告訴了黑客。

爲了不發生這種安全事故,設置

  • 給返回內容加上對應的 contentType。
  • 添加響應頭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

若是需求就是讓用戶下載 json 文件怎麼辦呢?

有另一個響應頭部字段 Conten-disposition 👹 ,Content-Disposition 指定響應的內容該以哪一種形式展現,是之內聯的形式(即網頁或者頁面的一部分),仍是以附件的形式下載並保存到本地,分別對應 inlineattachment

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',
  };
});
複製代碼

而後訪問剛纔的路由,就能看到文件下載下來了。

export

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。

async-download

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 處理。

download

擴展閱讀

吐槽

這篇文章本來標題叫《宇宙最強前端拖拽上傳和文件下載》,寫到一半查資料的時候發現掘金已經有不少人寫過相似的文章。

心態崩了,改稿已經來不及,就這樣吧。(浪費了大半天時間) 祝你們春節快樂,年終獎紅紅火火。

相關文章
相關標籤/搜索