聊一聊 15.5K 的 FileSaver,是如何工做的?

FileSaver.js 是在客戶端保存文件的解決方案,很是適合在客戶端上生成文件的 Web 應用程序。它簡單易用且兼容大多數瀏覽器,被做爲項目依賴應用在 6.3 萬的項目中。在近期的項目中,阿寶哥再一次使用到了它,因此就想寫篇文章來聊一聊這個優秀的開源項目。javascript

1、FileSaver.js 簡介

FileSaver.js 是 HTML5 的 saveAs() FileSaver 實現。它支持大多數主流的瀏覽器,其兼容性以下圖所示:html

(圖片來源:https://github.com/eligrey/Fi...前端

關注「全棧修仙之路」閱讀阿寶哥原創的 3 本免費電子書(累計下載近2萬)及 50 幾篇 「重學TS」 教程。

1.1 saveAs API

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

saveAs 方法支持三個參數,第一個參數表示它支持 Blob/File/Url 三種類型,第二個參數表示文件名(可選),而第三個參數表示配置對象(可選)。若是你須要 FlieSaver.js 自動提供 Unicode 文本編碼提示(參考:字節順序標記),則須要設置 { autoBom: true}java

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 地址與當前站點是同域的,則將使用 a[download] 方式下載。不然,會先使用 同步的 HEAD 請求 來判斷是否支持 CORS 機制,若支持的話,將進行數據下載並使用 Blob URL 實現文件下載。若是不支持 CORS 機制的話,將會嘗試使用 a[download] 方式下載。node

標準的 W3C File API Blob 接口並不是在全部瀏覽器中均可用,對於這個問題,你能夠考慮使用 Blob.js 來解決兼容性問題。git

(圖片來源:https://caniuse.com/?search=blobgithub

1.4 保存 Canvas 畫布內容

let canvas = document.getElementById("my-canvas");
canvas.toBlob(function(blob) {
  saveAs(blob, "abao.png");
});

須要注意的是 canvas.toBlob() 方法並不是在全部瀏覽器中均可用,對於這個問題,你能夠考慮使用 canvas-toBlob.js 來解決兼容性問題。web

(圖片來源:https://caniuse.com/?search=t...數據庫

在以上的示例中,咱們屢次見到 Blob 的身影,所以在介紹 FileSaver.js 源碼時,阿寶哥先來簡單介紹一下 Blob 的相關知識。canvas

2、Blob 簡介

Blob(Binary Large Object)表示二進制類型的大對象。在數據庫管理系統中,將二進制數據存儲爲一個單一個體的集合。Blob 一般是影像、聲音或多媒體文件。在 JavaScript 中 Blob 類型的對象表示不可變的相似文件對象的原始數據。

2.1 Blob 構造函數

Blob 由一個可選的字符串 type(一般是 MIME 類型)和 blobParts 組成:

MIME(Multipurpose Internet Mail Extensions)多用途互聯網郵件擴展類型,是設定某種擴展名的文件用一種應用程序來打開的方式類型,當該擴展名文件被訪問的時候,瀏覽器會自動使用指定應用程序來打開。多用於指定一些客戶端自定義的文件名,以及一些媒體文件打開方式。

常見的 MIME 類型有:超文本標記語言文本 .html text/html、PNG 圖像 .png image/png、普通文本 .txt text/plain 等。

在 JavaScript 中咱們能夠經過 Blob 的構造函數來建立 Blob 對象,Blob 構造函數的語法以下:

var aBlob = new Blob(blobParts, options);

相關的參數說明以下:

  • blobParts:它是一個由 ArrayBuffer,ArrayBufferView,Blob,DOMString 等對象構成的數組。DOMStrings 會被編碼爲 UTF-8。
  • options:一個可選的對象,包含如下兩個屬性:

    • type —— 默認值爲 "",它表明了將會被放入到 blob 中的數組內容的 MIME 類型。
    • endings —— 默認值爲 "transparent",用於指定包含行結束符 \n 的字符串如何被寫入。 它是如下兩個值中的一個: "native",表明行結束符會被更改成適合宿主操做系統文件系統的換行符,或者 "transparent",表明會保持 blob 中保存的結束符不變。

介紹完 Blob 以後,咱們再來介紹一下 Blob URL。

2.2 Blob URL

Blob URL/Object URL 是一種僞協議,容許 Blob 和 File 對象用做圖像,下載二進制數據連接等的 URL 源。在瀏覽器中,咱們使用 URL.createObjectURL 方法來建立 Blob URL,該方法接收一個 Blob 對象,併爲其建立一個惟一的 URL,其形式爲 blob:<origin>/<uuid>,對應的示例以下:

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

瀏覽器內部爲每一個經過 URL.createObjectURL 生成的 URL 存儲了一個 URL → Blob 映射。所以,此類 URL 較短,但能夠訪問 Blob。生成的 URL 僅在當前文檔打開的狀態下才有效。它容許引用 <img><a> 中的 Blob,但若是你訪問的 Blob URL 再也不存在,則會從瀏覽器中收到 404 錯誤。

上述的 Blob URL 看似很不錯,但實際上它也有反作用。 雖然存儲了 URL → Blob 的映射,但 Blob 自己仍駐留在內存中,瀏覽器沒法釋放它。映射在文檔卸載時自動清除,所以 Blob 對象隨後被釋放。可是,若是應用程序壽命很長,那不會很快發生。所以,若是咱們建立一個 Blob URL,即便再也不須要該 Blob,它也會存在內存中。

針對這個問題,咱們能夠調用 URL.revokeObjectURL(url) 方法,從內部映射中刪除引用,從而容許刪除 Blob(若是沒有其餘引用),並釋放內存。

好的,如今咱們已經介紹了 Blob 和 Blob URL。若是你還意猶未盡,想深刻理解 Blob 的話,能夠閱讀 你不知道的 Blob 這篇文章,接下來咱們開始分析 FileSaver.js 的源碼。

若是你想了解閱讀源碼的思路與技巧,能夠閱讀 使用這些思路與技巧,我讀懂了多個優秀的開源項目 這篇文章。

3、FileSaver.js 源碼解析

在 FileSaver.js 內部提供了三種方案來實現文件保存,所以接下來咱們將分別來介紹這三種方案。

3.1 方案一

當 FileSaver.js 在保存文件時,若是當前平臺中 a 標籤支持 download 屬性且非 MacOS WebView 環境,則會優先使用 a[download] 來實現文件保存。在具體使用過程當中,咱們是經過調用 saveAs 方法來保存文件,該方法的定義以下:

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

經過觀察 saveAs 方法的簽名,咱們可知該方法支持字符串和 Blob 兩種類型的參數,所以在 saveAs 方法內部須要分別處理這兩種類型的參數,下面咱們先來分析字符串參數的情形。

3.1.1 字符串類型參數

在前面的示例中,咱們演示瞭如何利用 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類型參數
  }
}

在以上代碼中,若是發現下載資源的 URL 地址與當前站點是非同域的,則會先使用 同步的 HEAD 請求 來判斷是否支持 CORS 機制,若支持的話,就會調用 download 方法進行文件下載。首先咱們先來分析 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 方法的實現很簡單,就是經過 XMLHttpRequest API 發起一個同步的 HEAD 請求,而後判斷返回的狀態碼是否在 [200 ~ 299] 的範圍內。接着咱們來看一下 download 方法的具體實現:

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 方法的實現也很簡單,也是經過 XMLHttpRequest API 來發起 HTTP 請求,與你們熟悉的 JSON 格式不一樣的是,咱們須要設置 responseType 的類型爲 blob。此外,由於返回的結果是 blob 類型的數據,因此在成功回調函數內部會繼續調用 saveAs 方法來實現文件保存。

而對於不支持 CORS 機制或同域的情形,它會調用內部的 click 方法來完成下載功能,該方法的具體實現以下:

// `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 方法內部,會優先調用 node 對象上的 dispatchEvent 方法來派發 click 事件。當出現異常的時候,會在 catch 語句進行相應的異常處理,catch 語句中的 MouseEvent.initMouseEvent() 方法用於初始化鼠標事件的值。但須要注意的是,該特性已經從 Web 標準中刪除,雖然一些瀏覽器目前仍然支持它,但也許會在將來的某個時間中止支持,請儘可能不要使用該特性

3.1.2 blob 類型參數

一樣,在前面的示例中,咱們演示瞭如何利用 saveAs 方法來保存 Blob 類型數據:

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

blob 類型參數的處理邏輯,被定義在 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") {
     // 省略處理字符串類型參數
  } else {
    a.href = URL.createObjectURL(blob);
    setTimeout(function () {
      URL.revokeObjectURL(a.href);
    }, 4e4); // 40s
    setTimeout(function () {
      click(a);
    }, 0);
  }
}

對於 blob 類型的參數,首先會經過 createObjectURL 方法來建立 Object URL,而後在經過 click 方法執行文件保存。爲了能及時釋放內存,在 else 處理分支中,會啓動一個定時器來執行清理操做。此時,方案一咱們已經介紹完了,接下去要介紹的方案二主要是爲了兼容 IE 瀏覽器。

3.2 方案二

在 Internet Explorer 10 瀏覽器中,msSaveBlob 和 msSaveOrOpenBlob 方法容許用戶在客戶端上保存文件,其中 msSaveBlob 方法只提供一個保存按鈕,而 msSaveOrOpenBlob 方法提供了保存和打開按鈕,對應的使用方式以下所示:

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

瞭解完上述的知識和方案一中介紹的 corsEnableddownloadclick 方法後,再來看方案二的代碼,就很清晰明瞭。在知足 "msSaveOrOpenBlob" in navigator 條件時, FileSaver.js 會使用方案二來實現文件保存。跟前面同樣,咱們先來分析 字符串類型參數 的處理邏輯。

3.2.1 字符串類型參數
// 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類型參數
  }
}
3.2.2 blob 類型參數
// Use msSaveOrOpenBlob as a second approach
function saveAs(blob, name, opts) {
  name = name || blob.name || "download";
  if (typeof blob === "string") {
    // 省略處理字符串類型參數
  } else {
    navigator.msSaveOrOpenBlob(bom(blob, opts), name); // 提供了保存和打開按鈕
  }
}

3.3 方案三

若是方案一和方案二都不支持的話,FileSaver.js 就會降級使用 FileReader API 和 open API 新開窗口來實現文件保存。

3.3.1 字符串類型參數
// 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類型參數
}
3.3.2 blob 類型參數

對於 blob 類型的參數來講,在 saveAs 方法內部會根據不一樣的環境選用不一樣的方案,好比在 Safari 瀏覽器環境中,它會利用 FileReader API 先把 Blob 對象轉換爲 Data URL,而後再把該 Data URL 地址賦值給新開的窗口或當前窗口的 location 對象,具體的代碼以下:

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

  var force = blob.type === "application/octet-stream"; // 二進制流數據
  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 {
    // 省略Object URL的處理邏輯
  }
}

其實對於 FileReader API 來講,除了支持把 File/Blob 對象轉換爲 Data URL 以外,它還提供了 readAsArrayBuffer()readAsText() 方法,用於把 File/Blob 對象轉換爲其它的數據格式。在 玩轉前端二進制 文章中,阿寶哥詳細介紹了 FileReader API 在前端圖片處理場景中的應用,閱讀完該文章以後,大家將能輕鬆看懂如下轉換關係圖:

最後咱們再來看一下 else 分支的代碼:

function saveAs(blob, name, opts, popup) {
  popup = popup || open("", "_blank");
  if (popup) {
    popup.document.title = popup.document.body.innerText = "downloading...";
  }

  // 處理字符串類型參數
  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 這個庫的源碼已經分析完成了,跟着阿寶哥閱讀上述源碼以後,是否是以爲寫一個兼容性好、簡單易用的第三方庫是多麼不容易。在實際項目中,若是你須要保存超過 blob 大小限制的超大文件,或者沒有足夠的內存空間,你能夠考慮使用更高級的 StreamSaver.js 庫來實現文件保存功能。

關注「全棧修仙之路」閱讀阿寶哥原創的 3 本免費電子書(累計下載近2萬)及 8 篇源碼分析系列教程。

4、參考資源

相關文章
相關標籤/搜索