聊一聊 15.5K 的 FileSaver,是如何工作的?
FileSaver.js 是在客戶端保存文件的解決方案,非常適合在客戶端上生成文件的 Web 應(yīng)用程序。它簡(jiǎn)單易用且兼容大多數(shù)瀏覽器,被作為項(xiàng)目依賴應(yīng)用在 6.3 萬的項(xiàng)目中。在近期的項(xiàng)目中,阿寶哥再一次使用到了它,所以就想寫篇文章來聊一聊這個(gè)優(yōu)秀的開源項(xiàng)目。
一、FileSaver.js 簡(jiǎn)介
FileSaver.js 是 HTML5 的 saveAs() FileSaver 實(shí)現(xiàn)。它支持大多數(shù)主流的瀏覽器,其兼容性如下圖所示:

(圖片來源:https://github.com/eligrey/FileSaver.js)
1.1 saveAs API
FileSaver?saveAs(Blob/File/Url,?optional?DOMString?filename,?optional?Object?{ autoBom })
saveAs 方法支持三個(gè)參數(shù),第一個(gè)參數(shù)表示它支持 Blob/File/Url 三種類型,第二個(gè)參數(shù)表示文件名(可選),而第三個(gè)參數(shù)表示配置對(duì)象(可選)。如果你需要 FlieSaver.js 自動(dòng)提供 Unicode 文本編碼提示(參考:字節(jié)順序標(biāo)記),則需要設(shè)置?{ autoBom: true}。
1.2 保存文本
let?blob?=?new?Blob(["大家好,我是阿寶哥!"],?{?type:?"text/plain;charset=utf-8"?});
FileSaver.saveAs(blob,?"hello.txt");
1.3 保存線上資源
FileSaver.saveAs("https://httpbin.org/image",?"image.jpg");
如果下載的 URL 地址與當(dāng)前站點(diǎn)是同域的,則將使用 a[download]?方式下載。否則,會(huì)先使用?同步的 HEAD 請(qǐng)求?來判斷是否支持 CORS 機(jī)制,若支持的話,將進(jìn)行數(shù)據(jù)下載并使用 Blob URL 實(shí)現(xiàn)文件下載。如果不支持 CORS 機(jī)制的話,將會(huì)嘗試使用 a[download]?方式下載。
標(biāo)準(zhǔn)的 W3C File API Blob 接口并非在所有瀏覽器中都可用,對(duì)于這個(gè)問題,你可以考慮使用 Blob.js 來解決兼容性問題。

(圖片來源:https://caniuse.com/?search=blob)
1.4 保存 Canvas 畫布內(nèi)容
let?canvas?=?document.getElementById("my-canvas");
canvas.toBlob(function(blob)?{
??saveAs(blob,?"abao.png");
});
需要注意的是 canvas.toBlob()?方法并非在所有瀏覽器中都可用,對(duì)于這個(gè)問題,你可以考慮使用 canvas-toBlob.js 來解決兼容性問題。

(圖片來源:https://caniuse.com/?search=toBlob)
在以上的示例中,我們多次見到 Blob 的身影,因此在介紹 FileSaver.js 源碼時(shí),阿寶哥先來簡(jiǎn)單介紹一下 Blob 的相關(guān)知識(shí)。
二、Blob 簡(jiǎn)介
Blob(Binary Large Object)表示二進(jìn)制類型的大對(duì)象。在數(shù)據(jù)庫管理系統(tǒng)中,將二進(jìn)制數(shù)據(jù)存儲(chǔ)為一個(gè)單一個(gè)體的集合。Blob 通常是影像、聲音或多媒體文件。在 JavaScript 中 Blob 類型的對(duì)象表示不可變的類似文件對(duì)象的原始數(shù)據(jù)。
2.1 Blob 構(gòu)造函數(shù)
Blob 由一個(gè)可選的字符串 type(通常是 MIME 類型)和 blobParts 組成:

MIME(Multipurpose Internet Mail Extensions)多用途互聯(lián)網(wǎng)郵件擴(kuò)展類型,是設(shè)定某種擴(kuò)展名的文件用一種應(yīng)用程序來打開的方式類型,當(dāng)該擴(kuò)展名文件被訪問的時(shí)候,瀏覽器會(huì)自動(dòng)使用指定應(yīng)用程序來打開。多用于指定一些客戶端自定義的文件名,以及一些媒體文件打開方式。
常見的 MIME 類型有:超文本標(biāo)記語言文本 .html text/html、PNG 圖像 .png image/png、普通文本 .txt text/plain 等。
在 JavaScript 中我們可以通過 Blob 的構(gòu)造函數(shù)來創(chuàng)建 Blob 對(duì)象,Blob 構(gòu)造函數(shù)的語法如下:
var?aBlob?=?new?Blob(blobParts,?options);
相關(guān)的參數(shù)說明如下:
blobParts:它是一個(gè)由 ArrayBuffer,ArrayBufferView,Blob,DOMString 等對(duì)象構(gòu)成的數(shù)組。DOMStrings 會(huì)被編碼為 UTF-8。 options:一個(gè)可選的對(duì)象,包含以下兩個(gè)屬性: type —— 默認(rèn)值為?"",它代表了將會(huì)被放入到 blob 中的數(shù)組內(nèi)容的 MIME 類型。 endings —— 默認(rèn)值為?"transparent",用于指定包含行結(jié)束符?\n 的字符串如何被寫入。它是以下兩個(gè)值中的一個(gè):?"native",代表行結(jié)束符會(huì)被更改為適合宿主操作系統(tǒng)文件系統(tǒng)的換行符,或者?"transparent",代表會(huì)保持 blob 中保存的結(jié)束符不變。
2.2 Blob URL
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
三、FileSaver.js 源碼解析
3.1 方案一
FileSaver?saveAs(Blob/File/Url,?optional?DOMString?filename,?optional?Object?{?autoBom?})
3.1.1 字符串類型參數(shù)
FileSaver.saveAs("https://httpbin.org/image",?"image.jpg");
//?Use?download?attribute?first?if?possible?(#193?Lumia?mobile)?unless?this?is?a?macOS?WebView
function?saveAs(blob,?name,?opts)?{
??var?URL?=?_global.URL?||?_global.webkitURL;
??var?a?=?document.createElement("a");
??name?=?name?||?blob.name?||?"download";
??a.download?=?name;
??a.rel?=?"noopener";
??if?(typeof?blob?===?"string")?{
????a.href?=?blob;
????if?(a.origin?!==?location.origin)?{?//?(1)
??????corsEnabled(a.href)
??????????download(blob,?name,?opts)
????????:?click(a,?(a.target?=?"_blank"));
????}?else?{?//?(2)
??????click(a);
????}
??}?else?{
????//?省略處理Blob類型參數(shù)
??}
}
function?corsEnabled(url)?{
??var?xhr?=?new?XMLHttpRequest();
??xhr.open("HEAD",?url,?false);
??try?{
????xhr.send();
??}?catch?(e)?{}
??return?xhr.status?>=?200?&&?xhr.status?<=?299;
}
function?download(url,?name,?opts)?{
??var?xhr?=?new?XMLHttpRequest();
??xhr.open("GET",?url);
??xhr.responseType?=?"blob";
??xhr.onload?=?function?()?{
????saveAs(xhr.response,?name,?opts);
??};
??xhr.onerror?=?function?()?{
????console.error("could?not?download?file");
??};
??xhr.send();
}
//?`a.click()`?doesn't?work?for?all?browsers?(#465)
function?click(node)?{
??try?{
????node.dispatchEvent(new?MouseEvent("click"));
??}?catch?(e)?{
????var?evt?=?document.createEvent("MouseEvents");
????evt.initMouseEvent(
??????"click",?true,?true,?window,?0,?0,?0,?80,?20,?
??????false,?false,?false,?false,?0,?null
????);
????node.dispatchEvent(evt);
??}
}
3.1.2 blob 類型參數(shù)
let?blob?=?new?Blob(["大家好,我是阿寶哥!"],?{?type:?"text/plain;charset=utf-8"?});
FileSaver.saveAs(blob,?"hello.txt");
//?Use?download?attribute?first?if?possible?(#193?Lumia?mobile)?unless?this?is?a?macOS?WebView
function?saveAs(blob,?name,?opts)?{
??var?URL?=?_global.URL?||?_global.webkitURL;
??var?a?=?document.createElement("a");
??name?=?name?||?blob.name?||?"download";
??a.download?=?name;
??a.rel?=?"noopener";
??if?(typeof?blob?===?"string")?{
?????//?省略處理字符串類型參數(shù)
??}?else?{
????a.href?=?URL.createObjectURL(blob);
????setTimeout(function?()?{
??????URL.revokeObjectURL(a.href);
????},?4e4);?//?40s
????setTimeout(function?()?{
??????click(a);
????},?0);
??}
}
3.2 方案二
window.navigator.msSaveBlob(blobObject,?'msSaveBlob_hello.txt');
window.navigator.msSaveOrOpenBlob(blobObject,?'msSaveBlobOrOpenBlob_hello.txt');
3.2.1 字符串類型參數(shù)
//?Use?msSaveOrOpenBlob?as?a?second?approach
function?saveAs(blob,?name,?opts)?{
??name?=?name?||?blob.name?||?"download";
??if?(typeof?blob?===?"string")?{
????if?(corsEnabled(blob))?{?//?判斷是否支持CORS
??????download(blob,?name,?opts);
????}?else?{
??????var?a?=?document.createElement("a");
??????a.href?=?blob;
??????a.target?=?"_blank";
??????setTimeout(function?()?{
????????click(a);
??????});
????}
??}?else?{
????//?省略處理Blob類型參數(shù)
??}
}
3.2.2 blob 類型參數(shù)
//?Use?msSaveOrOpenBlob?as?a?second?approach
function?saveAs(blob,?name,?opts)?{
??name?=?name?||?blob.name?||?"download";
??if?(typeof?blob?===?"string")?{
????//?省略處理字符串類型參數(shù)
??}?else?{
????navigator.msSaveOrOpenBlob(bom(blob,?opts),?name);?//?提供了保存和打開按鈕
??}
}
3.3 方案三
3.3.1 字符串類型參數(shù)
//?Fallback?to?using?FileReader?and?a?popup
function?saveAs(blob,?name,?opts,?popup)?{
??//?Open?a?popup?immediately?do?go?around?popup?blocker
??//?Mostly?only?available?on?user?interaction?and?the?fileReader?is?async?so...
??popup?=?popup?||?open("",?"_blank");
??if?(popup)?{
????popup.document.title?=?popup.document.body.innerText?=?"downloading...";
??}
??if?(typeof?blob?===?"string")?return?download(blob,?name,?opts);
????//?處理Blob類型參數(shù)
}
3.3.2 blob 類型參數(shù)
//?Fallback?to?using?FileReader?and?a?popup
function?saveAs(blob,?name,?opts,?popup)?{
??//?Open?a?popup?immediately?do?go?around?popup?blocker
??//?Mostly?only?available?on?user?interaction?and?the?fileReader?is?async?so...
??popup?=?popup?||?open("",?"_blank");
??if?(popup)?{?//?設(shè)置新開窗口的標(biāo)題
????popup.document.title?=?popup.document.body.innerText?=?"downloading...";
??}
??if?(typeof?blob?===?"string")?return?download(blob,?name,?opts);
??var?force?=?blob.type?===?"application/octet-stream";?//?二進(jìn)制流數(shù)據(jù)
??var?isSafari?=?/constructor/i.test(_global.HTMLElement)?||?_global.safari;
??var?isChromeIOS?=?/CriOS\/[\d]+/.test(navigator.userAgent);
??if?(
????(isChromeIOS?||?(force?&&?isSafari)?||?isMacOSWebView)?&&
????typeof?FileReader?!==?"undefined"
??)?{
????//?Safari?doesn't?allow?downloading?of?blob?URLs
????var?reader?=?new?FileReader();
????reader.onloadend?=?function?()?{
??????var?url?=?reader.result;
??????url?=?isChromeIOS
??????????url
????????:?url.replace(/^data:[^;]*;/,?"data:attachment/file;");?//?處理成附件的形式
??????if?(popup)?popup.location.href?=?url;
??????else?location?=?url;
??????popup?=?null;?//?reverse-tabnabbing?#460
????};
????reader.readAsDataURL(blob);
??}?else?{
????//?省略O(shè)bject?URL的處理邏輯
??}
}

function?saveAs(blob,?name,?opts,?popup)?{
??popup?=?popup?||?open("",?"_blank");
??if?(popup)?{
????popup.document.title?=?popup.document.body.innerText?=?"downloading...";
??}
??//?處理字符串類型參數(shù)
??if?(typeof?blob?===?"string")?return?download(blob,?name,?opts);
??if?(
????(isChromeIOS?||?(force?&&?isSafari)?||?isMacOSWebView)?&&
????typeof?FileReader?!==?"undefined"
??)?{
????//?省略FileReader?API處理邏輯
??}?else?{
????var?URL?=?_global.URL?||?_global.webkitURL;
????var?url?=?URL.createObjectURL(blob);
????if?(popup)?popup.location?=?url;
????else?location.href?=?url;
????popup?=?null;?//?reverse-tabnabbing?#460
????setTimeout(function?()?{
??????URL.revokeObjectURL(url);
????},?4e4);?//?40s
??}
}

