1. 程式人生 > 實用技巧 >你不知道的 Blob

你不知道的 Blob

來自公眾號:全棧修仙之路

如果你允許使用者從你的網站上下載某些檔案,那你可能會遇到 Blob 型別。為了實現上述的功能,你可以很容易從網上找到相關的示例,並根據實際需求進行適當的調整。對於部分開發者來說,在完成上述功能之後,他們並不會繼續思考 Blob 是什麼?

這就導致了一些開發者,還是停留在熟練使用 API 的層面,當遇到比較棘手的問題時,就束手無策。換句話說,如果當你在熟悉 API 的使用之後,還能繼續多問幾個為什麼,繼續探究下去,不僅能加深對知識的理解,還能觸類旁通,拓展自己的知識面提高自己。

好了,如果你想繼續瞭解什麼是 Blob,那麼就跟上我的腳步,來一個 Blob Web API 探索之旅。還在猶豫什麼,Let's go!

讀完本文你將瞭解到以下內容:

  • Blob 是什麼
  • Blob API 簡介
    • 建構函式
    • 屬性和方法
  • Blob 使用場景
    • 分片上傳
    • 從網際網路下載資料
    • Blob 用作 URL
    • Blob 轉換為 Base64
    • 圖片壓縮
    • 生成 PDF
  • Blob 與 ArrayBuffer 的區別

一、Blob 是什麼

Blob(Binary Large Object)表示二進位制型別的大物件。在資料庫管理系統中,將二進位制資料儲存為一個單一個體的集合。Blob 通常是影像、聲音或多媒體檔案。在 JavaScript 中 Blob 型別的物件表示不可變的類似檔案物件的原始資料。為了更直觀的感受 Blob 物件,我們先來使用 Blob 建構函式,建立一個 myBlob 物件,具體如下圖所示:

如你所見,myBlob 物件含有兩個屬性:size 和 type。其中size屬性用於表示資料的大小(以位元組為單位),type是 MIME 型別的字串。Blob 表示的不一定是 JavaScript 原生格式的資料。比如File介面基於Blob,繼承了 blob 的功能並將其擴充套件使其支援使用者系統上的檔案。

二、Blob API 簡介

Blob由一個可選的字串type(通常是 MIME 型別)和blobParts組成:

MIME(Multipurpose Internet Mail Extensions)多用途網際網路郵件擴充套件型別,是設定某種副檔名的檔案用一種應用程式來開啟的方式型別,當該副檔名檔案被訪問的時候,瀏覽器會自動使用指定應用程式來開啟。多用於指定一些客戶端自定義的檔名,以及一些媒體檔案開啟方式。

常見的 MIME 型別有:超文字標記語言文字 .html text/html、PNG影象 .png image/png、普通文字 .txt text/plain 等。

2.1 建構函式

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

let myBlobParts = ['<html><h2>Hello Semlinker</h2></html>']; // an array consisting of a single DOMString
let myBlob = new Blob(myBlobParts, {type : 'text/html', endings: "transparent"}); // the blob

console.log(myBlob.size + " bytes size");
// Output: 37 bytes size
console.log(myBlob.type + " is the type");
// Output: text/html is the type

示例二:從型別化陣列和字串建立 Blob

let hello = new Uint8Array([72, 101, 108, 108, 111]); // 二進位制格式的 "hello"
let blob = new Blob([hello, ' ', 'semlinker'], {type: 'text/plain'});

介紹完 Blob 建構函式,接下來我們來分別介紹 Blob 類的屬性和方法:

2.2 屬性

前面我們已經知道 Blob 物件包含兩個屬性:

  • size(只讀):表示Blob物件中所包含資料的大小(以位元組為單位)。
  • type(只讀):一個字串,表明該Blob物件所包含資料的 MIME 型別。如果型別未知,則該值為空字串。

2.3 方法

  • slice([start[, end[, contentType]]]):返回一個新的 Blob 物件,包含了源 Blob 物件中指定範圍內的資料。
  • stream():返回一個能讀取 blob 內容的ReadableStream
  • text():返回一個 Promise 物件且包含 blob 所有內容的 UTF-8 格式的USVString
  • arrayBuffer():返回一個 Promise 物件且包含 blob 所有內容的二進位制格式的ArrayBuffer

這裡我們需要注意的是,Blob物件是不可改變的。我們不能直接在一個 Blob 中更改資料,但是我們可以對一個 Blob 進行分割,從其中建立新的 Blob 物件,將它們混合到一個新的 Blob 中。這種行為類似於 JavaScript 字串:我們無法更改字串中的字元,但可以建立新的更正後的字串。

三、Blob 使用場景

3.1 分片上傳

File 物件是特殊型別的 Blob,且可以用在任意的 Blob 型別的上下文中。所以針對大檔案傳輸的場景,我們可以使用 slice 方法對大檔案進行切割,然後分片進行上傳,具體示例如下:

const file = new File(["a".repeat(1000000)], "test.txt");

const chunkSize = 40000;
const url = "https://httpbin.org/post";

async function chunkedUpload() {
  for (let start = 0; start < file.size; start += chunkSize) {
      const chunk = file.slice(start, start + chunkSize + 1);
      const fd = new FormData();
      fd.append("data", chunk);

      await fetch(url, { method: "post", body: fd }).then((res) =>
        res.text()
      );
  }
}

3.2 從網際網路下載資料

我們可以使用以下方法從網際網路上下載資料並將資料儲存到 Blob 物件中,比如:

const downloadBlob = (url, callback) => {
 const xhr = new XMLHttpRequest()
 xhr.open('GET', url)
 xhr.responseType = 'blob'
 xhr.onload = () => {
    callback(xhr.response)
 }
 xhr.send(null)
}

當然除了使用XMLHttpRequestAPI 之外,我們也可以使用fetchAPI 來實現以流的方式獲取二進位制資料。這裡我們來看一下如何使用 fetch API 獲取線上圖片並本地顯示,具體實現如下:

const myImage = document.querySelector('img');
const myRequest = new Request('flowers.jpg');

fetch(myRequest)
  .then(function(response) {
    return response.blob();
  })
 .then(function(myBlob) {
   let objectURL = URL.createObjectURL(myBlob);
   myImage.src = objectURL;
});

當 fetch 請求成功的時候,我們呼叫 response 物件的blob()方法,從 response 物件中讀取一個 Blob 物件,然後使用createObjectURL()方法建立一個 objectURL,然後把它賦值給img元素的src屬性從而顯示這張圖片。

3.3 Blob 用作 URL

Blob 可以很容易的作為<a><img>或其他標籤的 URL,多虧了type屬性,我們也可以上傳/下載Blob物件。下面我們將舉一個 Blob 檔案下載的示例,不過在看具體示例前我們得簡單介紹一下 Blob URL。

1.Blob URL/Object 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 檔案下載的具體示例。

2.Blob 檔案下載示例

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Blob 檔案下載示例</title>
  </head>

  <body>
    <button id="downloadBtn">檔案下載</button>
    <script src="index.js"></script>
  </body>
</html>

index.js

const download = (fileName, blob) => {
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
  link.remove();
  URL.revokeObjectURL(link.href);
};

const downloadBtn = document.querySelector("#downloadBtn");
downloadBtn.addEventListener("click", (event) => {
  const fileName = "blob.txt";
  const myBlob = new Blob(["一文徹底掌握 Blob Web API"], { type: "text/plain" });
  download(fileName, myBlob);
});

在示例中,我們通過呼叫 Blob 的建構函式來建立型別為"text/plain"的 Blob 物件,然後通過動態建立a標籤來實現檔案的下載。

3.4 Blob 轉換為 Base64

URL.createObjectURL的一個替代方法是,將Blob轉換為 base64 編碼的字串。Base64是一種基於 64 個可列印字元來表示二進位制資料的表示方法,它常用於在處理文字資料的場合,表示、傳輸、儲存一些二進位制資料,包括 MIME 的電子郵件及 XML 的一些複雜資料。

在 MIME 格式的電子郵件中,base64 可以用來將二進位制的位元組序列資料編碼成 ASCII 字元序列構成的文字。使用時,在傳輸編碼方式中指定 base64。使用的字元包括大小寫拉丁字母各 26 個、數字 10 個、加號 + 和斜槓 /,共 64 個字元,等號 = 用來作為字尾用途。

下面我們來介紹如何在 HTML 中嵌入 base64 編碼的圖片。在編寫 HTML 網頁時,對於一些簡單圖片,通常會選擇將圖片內容直接內嵌在網頁中,從而減少不必要的網路請求,但是圖片資料是二進位制資料,該怎麼嵌入呢?絕大多數現代瀏覽器都支援一種名為Data URLs的特性,允許使用 base64 對圖片或其他檔案的二進位制資料進行編碼,將其作為文字字串嵌入網頁中。

Data URLs 由四個部分組成:字首(data:)、指示資料型別的 MIME 型別、如果非文字則為可選的base64標記、資料本身:

data:[<mediatype>][;base64],<data>

mediatype是個 MIME 型別的字串,例如 "image/jpeg" 表示 JPEG 影象檔案。如果被省略,則預設值為text/plain;charset=US-ASCII。如果資料是文字型別,你可以直接將文字嵌入(根據文件型別,使用合適的實體字元或轉義字元)。如果是二進位制資料,你可以將資料進行 base64 編碼之後再進行嵌入。比如嵌入一張圖片:

<imgalt="logo"src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...">

但需要注意的是:如果圖片較大,圖片的色彩層次比較豐富,則不適合使用這種方式,因為該圖片經過 base64 編碼後的字串非常大,會明顯增大 HTML 頁面的大小,從而影響載入速度。除此之外,利用 FileReader API,我們也可以方便的實現圖片本地預覽功能,具體程式碼如下:

<input type="file" accept="image/*" onchange="loadFile(event)">
<img id="output"/>

<script>
  const loadFile = function(event) {
    const reader = new FileReader();
    reader.onload = function(){
      const output = document.querySelector('output');
      output.src = reader.result;
    };
    reader.readAsDataURL(event.target.files[0]);
  };
</script>

在以上示例中,我們為 file 型別輸入框繫結onchange事件處理函式loadFile,在該函式中,我們建立了一個 FileReader 物件併為該物件繫結onload相應的事件處理函式,然後呼叫 FileReader 物件的readAsDataURL()方法,把本地圖片對應的 File 物件轉換為 Data URL。

在完成本地圖片預覽之後,我們可以直接把圖片對應的 Data URLs 資料提交到伺服器。針對這種情形,服務端需要做一些相關處理,才能正常儲存上傳的圖片,這裡以 Express 為例,具體處理程式碼如下:

const app = require('express')();

app.post('/upload', function(req, res){
    let imgData = req.body.imgData; // 獲取POST請求中的base64圖片資料
    let base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
    let dataBuffer = Buffer.from(base64Data, 'base64');
    fs.writeFile("image.png", dataBuffer, function(err) {
        if(err){
          res.send(err);
        }else{
          res.send("圖片上傳成功!");
        }
    });
});

對於 FileReader 物件來說,除了支援把 Blob/File 物件轉換為 Data URL 之外,它還提供了
readAsArrayBuffer()readAsText()方法,用於把 Blob/File 物件轉換為其它的資料格式。這裡我們來看個readAsArrayBuffer()的使用示例:
// 從 blob 獲取 arrayBuffer
let fileReader = new FileReader();

fileReader.onload = function(event) {
  let arrayBuffer = fileReader.result;
};
fileReader.readAsArrayBuffer(blob);

3.5 圖片壓縮

在一些場合中,我們希望在上傳本地圖片時,先對圖片進行一定的壓縮,然後再提交到伺服器,從而減少傳輸的資料量。在前端要實現圖片壓縮,我們可以利用 Canvas 物件提供的toDataURL()方法,該方法接收typeencoderOptions兩個可選引數。

其中type表示圖片格式,預設為image/png。而encoderOptions用於表示圖片的質量,在指定圖片格式為image/jpegimage/webp的情況下,可以從 0 到 1 的區間內選擇圖片的質量。如果超出取值範圍,將會使用預設值0.92,其他引數會被忽略。

下面我們來看一下具體如何實現圖片壓縮:

// compress.js
const MAX_WIDTH = 800; // 圖片最大寬度

function compress(base64, quality, mimeType) {
  let canvas = document.createElement("canvas");
  let img = document.createElement("img");
  img.crossOrigin = "anonymous";
  return new Promise((resolve, reject) => {
    img.src = base64;
    img.onload = () => {
      let targetWidth, targetHeight;
      if (img.width > MAX_WIDTH) {
        targetWidth = MAX_WIDTH;
        targetHeight = (img.height * MAX_WIDTH) / img.width;
      } else {
        targetWidth = img.width;
        targetHeight = img.height;
      }
      canvas.width = targetWidth;
      canvas.height = targetHeight;
      let ctx = canvas.getContext("2d");
      ctx.clearRect(0, 0, targetWidth, targetHeight); // 清除畫布
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      let imageData = canvas.toDataURL(mimeType, quality / 100);
      resolve(imageData);
    };
  });
}

對於返回的 Data URL 格式的圖片資料,為了進一步減少傳輸的資料量,我們可以把它轉換為 Blob 物件:
function dataUrlToBlob(base64, mimeType) {
  let bytes = window.atob(base64.split(",")[1]);
  let ab = new ArrayBuffer(bytes.length);
  let ia = new Uint8Array(ab);
  for (let i = 0; i < bytes.length; i++) {
    ia[i] = bytes.charCodeAt(i);
  }
  return new Blob([ab], { type: mimeType });
}

在轉換完成後,我們就可以壓縮後的圖片對應的 Blob 物件封裝在 FormData 物件中,然後再通過 AJAX 提交到伺服器上:

function uploadFile(url, blob) {
  let formData = new FormData();
  let request = new XMLHttpRequest();
  formData.append("image", blob);
  request.open("POST", url, true);
  request.send(formData);
}

其實 Canvas 物件除了提供toDataURL()方法之外,它還提供了一個toBlob()方法,該方法的語法如下:

canvas.toBlob(callback, mimeType, qualityArgument)

toDataURL()方法相比,toBlob()方法是非同步的,因此多了個callback引數,這個callback回撥方法預設的第一個引數就是轉換好的blob檔案資訊。

介紹完上述的內容,我們來看一下本地圖片壓縮完整的示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>本地圖片壓縮</title>
  </head>
  <body>
    <input type="file" accept="image/*" onchange="loadFile(event)" />
    <script src="./compress.js"></script>
    <script>
      const loadFile = function (event) {
        const reader = new FileReader();
        reader.onload = async function () {
          let compressedDataURL = await compress(
            reader.result,
            90,
            "image/jpeg"
          );
          let compressedImageBlob = dataUrlToBlob(compressedDataURL);
          uploadFile("https://httpbin.org/post", compressedImageBlob);
        };
        reader.readAsDataURL(event.target.files[0]);
      };
    </script>
  </body>
</html>

3.6 生成 PDF 文件

PDF(行動式檔案格式,Portable Document Format)是由 Adobe Systems 在 1993 年用於檔案交換所發展出的檔案格式。在瀏覽器端,利用一些現成的開源庫,比如 jsPDF,我們也可以方便地生成 PDF 文件。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>客戶端生成 PDF 示例</title>
  </head>
  <body>
    <h3>客戶端生成 PDF 示例</h3>
    <script src="https://unpkg.com/jspdf@latest/dist/jspdf.min.js"></script>
    <script>
      (function generatePdf() {
        const doc = new jsPDF();
        doc.text("Hello semlinker!", 66, 88);
        const blob = new Blob([doc.output()], { type: "application/pdf" });
        blob.text().then((blobAsText) => {
          console.log(blobAsText);
        });
      })();
    </script>
  </body>
</html>

在以上示例中,我們首先建立 PDF 文件物件,然後呼叫該物件上的text()方法在指定的座標點上新增Hello semlinker!文字,然後我們利用生成的 PDF 內容來建立對應的 Blob 物件,需要注意的是我們設定 Blob 的型別為application/pdf,最後我們把 Blob 物件中儲存的內容轉換為文字並輸出到控制檯。由於內容較多,這裡我們只列出少部分輸出結果:

%PDF-1.3
%ºß¬à
3 0 obj
<</Type /Page
/Parent 1 0 R
/Resources 2 0 R
/MediaBox [0 0 595.28 841.89]
/Contents 4 0 R
>>
endobj
....

其實 jsPDF 除了支援純文字之外,它也可以生成帶圖片的 PDF 文件,比如:

let imgData = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/...'
let doc = new jsPDF();

doc.setFontSize(40)
doc.text(35, 25, 'Paranyan loves jsPDF')
doc.addImage(imgData, 'JPEG', 15, 40, 180, 160)

Blob 的應用場景還很多,這裡我們就不一一列舉了,感興趣的小夥伴可以自行查閱相關資料。

四、Blob 與 ArrayBuffer 的區別

ArrayBuffer物件用於表示通用的,固定長度的原始二進位制資料緩衝區。你不能直接操縱 ArrayBuffer 的內容,而是需要建立一個型別化陣列物件或 DataView 物件,該物件以特定格式表示緩衝區,並使用該物件讀取和寫入緩衝區的內容。

Blob型別的物件表示不可變的類似檔案物件的原始資料。Blob 表示的不一定是 JavaScript 原生格式的資料。File 介面基於 Blob,繼承了Blob 功能並將其擴充套件為支援使用者系統上的檔案。

4.1 Blob vs ArrayBuffer

  • 除非你需要使用 ArrayBuffer 提供的寫入/編輯的能力,否則 Blob 格式可能是最好的。
  • Blob 物件是不可變的,而 ArrayBuffer 是可以通過 TypedArrays 或 DataView 來操作。
  • ArrayBuffer 是存在記憶體中的,可以直接操作。而 Blob 可以位於磁碟、快取記憶體記憶體和其他不可用的位置。
  • 雖然 Blob 可以直接作為引數傳遞給其他函式,比如window.URL.createObjectURL()。但是,你可能仍需要 FileReader 之類的 File API 才能與 Blob 一起使用。
  • Blob 與 ArrayBuffer 物件之間是可以相互轉化的:
    • 使用 FileReader 的readAsArrayBuffer()方法,可以把 Blob 物件轉換為 ArrayBuffer 物件;
    • 使用 Blob 建構函式,如new Blob([new Uint8Array(data]);,可以把 ArrayBuffer 物件轉換為 Blob 物件。

對於 HTTP 的場景,比如在 AJAX 場景下,BlobArrayBuffer可以通過以下方式來使用:

function GET(url, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.responseType = 'arraybuffer'; // or xhr.responseType = "blob";
  xhr.send();

  xhr.onload = function(e) {
    if (xhr.status != 200) {
      alert("Unexpected status code " + xhr.status + " for " + url);
      return false;
    }
    callback(new Uint8Array(xhr.response)); // or new Blob([xhr.response]);
  };
}

對於ArrayBufferUint8Array感興趣的讀者,可以閱讀Deno bytes 模組全解析這篇文章。

瞭解完上述的內容,相信有的讀者可能會覺得意猶未盡。那麼,對於 Blob 來說還有哪些內容可以繼續深入學習的呢?本人下一步的計劃是基於 Deno 的原始碼,來逐步分析 DenoBlob 的具體實現。當然也會順便分析一下URL.createObjectURL()方法和revokeObjectURL()方法的實現。

五、參考資源

  • MDN - Blob
  • MDN - Data URLs
  • javascript.info - blob
  • flaviocopes - blob
  • arraybuffer-vs-blob
  • javascript-interview-question-what-is-a-blob