這篇文章聊聊跟前端檔案下載相關的一些知識。
說到前端下載檔案,我最先想到的是在學校的時候,自己搭建 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
表示所有其他情況的預設值。一種未知的檔案型別應當使用此型別。
?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 啊,我們應該展現直接展示出來。???
使用者看到可愛的大熊貓同時,順便把個人資訊也告訴了黑客。
為了避免發生這種安全事故,設定
- 給返回內容加上對應的 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 檔案怎麼辦呢?
有另外一個響應頭部欄位 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`,
};
});
複製程式碼
然後訪問剛才的路由,就能看到檔案下載下來了。
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(`,`) + `
`;
}, ``);
const blob = new Blob([data]);
const url = URL.createObjectURL(blob);
downloadAs(url, `mock.csv`);
複製程式碼
相容性
download 屬性的相容性並不高,目前只有只有 80%。可以直接使用 FileSaver.js 做了 fallback 處理。
擴充套件閱讀
吐槽
這篇文章原本標題叫《宇宙最強前端拖拽上傳和檔案下載》,寫到一半查資料的時候發現掘金已經有很多人寫過類似的文章。
心態崩了,改稿已經來不及,就這樣吧。(浪費了大半天時間)
祝大家春節快樂,年終獎紅紅火火。