<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【源碼】793- 聊一聊 15.5K 的 FileSaver,是如何工作的?

          共 4447字,需瀏覽 9分鐘

           ·

          2020-12-03 12:51

          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é)束符不變。

          介紹完 Blob 之后,我們?cè)賮斫榻B一下 Blob URL。

          2.2 Blob URL

          Blob URL/Object URL 是一種偽協(xié)議,允許 Blob 和 File 對(duì)象用作圖像,下載二進(jìn)制數(shù)據(jù)鏈接等的 URL 源。在瀏覽器中,我們使用 URL.createObjectURL 方法來創(chuàng)建 Blob URL,該方法接收一個(gè) Blob 對(duì)象,并為其創(chuàng)建一個(gè)唯一的 URL,其形式為 blob:/,對(duì)應(yīng)的示例如下:

          blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

          瀏覽器內(nèi)部為每個(gè)通過 URL.createObjectURL 生成的 URL 存儲(chǔ)了一個(gè) URL → Blob 映射。因此,此類 URL 較短,但可以訪問 Blob。生成的 URL 僅在當(dāng)前文檔打開的狀態(tài)下才有效。它允許引用 中的 Blob,但如果你訪問的 Blob URL 不再存在,則會(huì)從瀏覽器中收到 404 錯(cuò)誤。

          上述的 Blob URL 看似很不錯(cuò),但實(shí)際上它也有副作用。雖然存儲(chǔ)了 URL → Blob 的映射,但 Blob 本身仍駐留在內(nèi)存中,瀏覽器無法釋放它。映射在文檔卸載時(shí)自動(dòng)清除,因此 Blob 對(duì)象隨后被釋放。但是,如果應(yīng)用程序壽命很長(zhǎng),那不會(huì)很快發(fā)生。因此,如果我們創(chuàng)建一個(gè) Blob URL,即使不再需要該 Blob,它也會(huì)存在內(nèi)存中。

          針對(duì)這個(gè)問題,我們可以調(diào)用 URL.revokeObjectURL(url) 方法,從內(nèi)部映射中刪除引用,從而允許刪除 Blob(如果沒有其他引用),并釋放內(nèi)存。

          好的,現(xiàn)在我們已經(jīng)介紹了 Blob 和 Blob URL。如果你還意猶未盡,想深入理解 Blob 的話,可以閱讀 你不知道的 Blob 這篇文章,接下來我們開始分析 FileSaver.js 的源碼。

          如果你想了解閱讀源碼的思路與技巧,可以閱讀 使用這些思路與技巧,我讀懂了多個(gè)優(yōu)秀的開源項(xiàng)目 這篇文章。

          三、FileSaver.js 源碼解析

          在 FileSaver.js 內(nèi)部提供了三種方案來實(shí)現(xiàn)文件保存,因此接下來我們將分別來介紹這三種方案。

          3.1 方案一

          當(dāng) FileSaver.js 在保存文件時(shí),如果當(dāng)前平臺(tái)中 a 標(biāo)簽支持 download 屬性且非 MacOS WebView 環(huán)境,則會(huì)優(yōu)先使用 a[download] 來實(shí)現(xiàn)文件保存。在具體使用過程中,我們是通過調(diào)用 saveAs 方法來保存文件,該方法的定義如下:

          FileSaver?saveAs(Blob/File/Url,?optional?DOMString?filename,?optional?Object?{?autoBom?})

          通過觀察 saveAs 方法的簽名,我們可知該方法支持字符串和 Blob 兩種類型的參數(shù),因此在 saveAs 方法內(nèi)部需要分別處理這兩種類型的參數(shù),下面我們先來分析字符串參數(shù)的情形。

          3.1.1 字符串類型參數(shù)

          在前面的示例中,我們演示了如何利用 saveAs 方法來保存線上的圖片:

          FileSaver.saveAs("https://httpbin.org/image",?"image.jpg");

          在方案一中,saveAs 方法的處理邏輯如下所示:

          //?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ù)
          ??}
          }

          在以上代碼中,如果發(fā)現(xiàn)下載資源的 URL 地址與當(dāng)前站點(diǎn)是非同域的,則會(huì)先使用 同步的 HEAD 請(qǐng)求 來判斷是否支持 CORS 機(jī)制,若支持的話,就會(huì)調(diào)用 download 方法進(jìn)行文件下載。首先我們先來分析 corsEnabled 方法:

          function?corsEnabled(url)?{
          ??var?xhr?=?new?XMLHttpRequest();
          ??xhr.open("HEAD",?url,?false);
          ??try?{
          ????xhr.send();
          ??}?catch?(e)?{}
          ??return?xhr.status?>=?200?&&?xhr.status?<=?299;
          }

          corsEnabled 方法的實(shí)現(xiàn)很簡(jiǎn)單,就是通過 XMLHttpRequest API 發(fā)起一個(gè)同步的 HEAD 請(qǐng)求,然后判斷返回的狀態(tài)碼是否在 [200 ~ 299] 的范圍內(nèi)。接著我們來看一下 download 方法的具體實(shí)現(xiàn):

          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();
          }

          同樣 download 方法的實(shí)現(xiàn)也很簡(jiǎn)單,也是通過 XMLHttpRequest API 來發(fā)起 HTTP 請(qǐng)求,與大家熟悉的 ?JSON 格式不同的是,我們需要設(shè)置 responseType 的類型為 blob。此外,因?yàn)榉祷氐慕Y(jié)果是 blob 類型的數(shù)據(jù),所以在成功回調(diào)函數(shù)內(nèi)部會(huì)繼續(xù)調(diào)用 saveAs 方法來實(shí)現(xiàn)文件保存。

          而對(duì)于不支持 CORS 機(jī)制或同域的情形,它會(huì)調(diào)用內(nèi)部的 click 方法來完成下載功能,該方法的具體實(shí)現(xiàn)如下:

          //?`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);
          ??}
          }

          click 方法內(nèi)部,會(huì)優(yōu)先調(diào)用 node 對(duì)象上的 dispatchEvent 方法來派發(fā) click 事件。當(dāng)出現(xiàn)異常的時(shí)候,會(huì)在 catch 語句進(jìn)行相應(yīng)的異常處理,catch 語句中的 MouseEvent.initMouseEvent() 方法用于初始化鼠標(biāo)事件的值。但需要注意的是,該特性已經(jīng)從 Web 標(biāo)準(zhǔn)中刪除,雖然一些瀏覽器目前仍然支持它,但也許會(huì)在未來的某個(gè)時(shí)間停止支持,請(qǐng)盡量不要使用該特性

          3.1.2 blob 類型參數(shù)

          同樣,在前面的示例中,我們演示了如何利用 saveAs 方法來保存 Blob 類型數(shù)據(jù):

          let?blob?=?new?Blob(["大家好,我是阿寶哥!"],?{?type:?"text/plain;charset=utf-8"?});
          FileSaver.saveAs(blob,?"hello.txt");

          blob 類型參數(shù)的處理邏輯,被定義在 saveAs 方法體的 else 分支中:

          //?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);
          ??}
          }

          對(duì)于 blob 類型的參數(shù),首先會(huì)通過 createObjectURL 方法來創(chuàng)建 Object URL,然后在通過 click 方法執(zhí)行文件保存。為了能及時(shí)釋放內(nèi)存,在 else 處理分支中,會(huì)啟動(dòng)一個(gè)定時(shí)器來執(zhí)行清理操作。此時(shí),方案一我們已經(jīng)介紹完了,接下去要介紹的方案二主要是為了兼容 IE 瀏覽器。

          3.2 方案二

          在 Internet Explorer 10 瀏覽器中,msSaveBlob 和 msSaveOrOpenBlob 方法允許用戶在客戶端上保存文件,其中 msSaveBlob 方法只提供一個(gè)保存按鈕,而 msSaveOrOpenBlob 方法提供了保存和打開按鈕,對(duì)應(yīng)的使用方式如下所示:

          window.navigator.msSaveBlob(blobObject,?'msSaveBlob_hello.txt');
          window.navigator.msSaveOrOpenBlob(blobObject,?'msSaveBlobOrOpenBlob_hello.txt');

          了解完上述的知識(shí)和方案一中介紹的 corsEnableddownloadclick 方法后,再來看方案二的代碼,就很清晰明了。

          在滿足 "msSaveOrOpenBlob" in navigator 條件時(shí), FileSaver.js 會(huì)使用方案二來實(shí)現(xiàn)文件保存。跟前面一樣,我們先來分析 字符串類型參數(shù) 的處理邏輯。

          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 方案三

          如果方案一和方案二都不支持的話,F(xiàn)ileSaver.js 就會(huì)降級(jí)使用 FileReader API 和 open API 新開窗口來實(shí)現(xiàn)文件保存。

          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ù)

          對(duì)于 blob 類型的參數(shù)來說,在 saveAs 方法內(nèi)部會(huì)根據(jù)不同的環(huán)境選用不同的方案,比如在 Safari 瀏覽器環(huán)境中,它會(huì)利用 FileReader API 先把 Blob 對(duì)象轉(zhuǎn)換為 Data URL,然后再把該 Data URL 地址賦值給新開的窗口或當(dāng)前窗口的 location 對(duì)象,具體的代碼如下:

          //?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的處理邏輯
          ??}
          }

          其實(shí)對(duì)于 FileReader API 來說,除了支持把 File/Blob 對(duì)象轉(zhuǎn)換為 Data URL 之外,它還提供了 readAsArrayBuffer()readAsText() 方法,用于把 File/Blob 對(duì)象轉(zhuǎn)換為其它的數(shù)據(jù)格式。

          玩轉(zhuǎn)前端二進(jìn)制 文章中,阿寶哥詳細(xì)介紹了 FileReader API 在前端圖片處理場(chǎng)景中的應(yīng)用,閱讀完該文章之后,你們將能輕松看懂以下轉(zhuǎn)換關(guān)系圖:

          最后我們?cè)賮砜匆幌?else 分支的代碼:

          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
          ??}
          }

          到這里 FileSaver.js 這個(gè)庫的源碼已經(jīng)分析完成了,跟著阿寶哥閱讀上述源碼之后,是不是覺得寫一個(gè)兼容性好、簡(jiǎn)單易用的第三方庫是多么不容易。

          在實(shí)際項(xiàng)目中,如果你需要保存超過 blob 大小限制的超大文件,或者沒有足夠的內(nèi)存空間,你可以考慮使用更高級(jí)的 StreamSaver.js 庫來實(shí)現(xiàn)文件保存功能。

          四、參考資源

          聚焦全棧,專注分享 TypeScript、Web API、前端架構(gòu)等技術(shù)干貨。

          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  激情青青草| 九色在线入口 | 免费a∨在线观看网站 | 日韩城人免费 | 青娱乐免费视频一二三 |