你不知道的 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) }
當然除了使用XMLHttpRequest
API 之外,我們也可以使用fetch
API 來實現以流的方式獲取二進位制資料。這裡我們來看一下如何使用 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()
方法,該方法接收type
和encoderOptions
兩個可選引數。
其中type
表示圖片格式,預設為image/png
。而encoderOptions
用於表示圖片的質量,在指定圖片格式為image/jpeg
或image/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 場景下,Blob和ArrayBuffer可以通過以下方式來使用:
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]); }; }
對於ArrayBuffer和Uint8Array感興趣的讀者,可以閱讀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