天天看點

淺談JS處理檔案資料的API:Blob、FileReader、Base64、File?

作者:第1024次Debug

在前端開發中,開發者常常會使用檔案上傳、下載下傳或資料轉換的操作。JavaScript 提供了許多 API 來處理二進制資料和轉換資料流,如:ArrayBuffer、FileReader、base64 、Blob、File等,他們之間的轉換與聯系如下所示。

Blob

Blob (binary large object) ,即二進制大對象,表示一個不可變、原始資料的類檔案對象。它的資料可以按文本或二進制的格式進行讀取,在 web 網頁中的充當資料流使用的相當頻繁。

blob 對象上有兩個屬性:

  • size:Blob對象中所包含資料的大小(位元組);
  • type:字元串,認為該Blob對象所包含的 MIME 類型。如果類型未知,則為空字元串。
let blob = new Blob([1,2,3,4,5], {type: "text/plain"});
// 可以 進行分割切片,如下
const _subBlob = blob.slice(0, 3,{type: "text/plain"});  // 此時切片的type 預設可以省略
URL.createObjectURL(blob);             //12345
URL.createObjectURL(_subBlob);     // 123           

注意: 如果Blob的資料屬于Unicode 編碼中的 Supplementary Plane(輔助平面) 字元,也即是需要用兩個 16 位的 UTF-16 編碼單元(稱為代理對或代理字元)來表示,那麼轉成的createObjectURL 連結不能擷取正确資料,獲得的是亂碼,此時可以使用FileReader進行擷取特定編碼格式的資料,如下:

let blob = new Blob(['!@#¥%']);
const _subBlob_o = blob.slice(0, 1);
const _subBlob_t = blob.slice(0, 2);
const _subBlob_th = blob.slice(0, 3);
URL.createObjectURL(_subBlob_o);      // �
URL.createObjectURL(_subBlob_t);     // 锛
URL.createObjectURL(_subBlob_th);     // 锛�

// 以下使用 FileReader 擷取UTF-8類型的text文本資料
let blob = new Blob(['!¥%&*']);
let _subBlob_o = blob.slice(0,3);
let _subBlob_t = blob.slice(3,6);
let _subBlob_th = blob.slice(6,7);
let _subBlob_f = blob.slice(7,8);
let fileReader = new FileReader();

fileReader.onload = function(event) {
  let result = event.target.result;
  console.log(result); // 這裡列印出讀取的資料,應該正确顯示UTF-8編碼的位元組字元
};

// fileReader.readAsText(blob, 'UTF-8');              // output:   !(中文歎号占用三個位元組)
// fileReader.readAsText(_subBlob_o, 'UTF-8');   // output:   ¥(中文$占用三個位元組)
// fileReader.readAsText(_subBlob_t, 'UTF-8');    // output:   %  
// fileReader.readAsText(_subBlob_th, 'UTF-8');  // output:   &           

File

檔案(File)Api 是一種用來描述有關檔案資訊的對象類型,File 對象本質上是繼承了 Blob 的一種封裝之後的對象 (包含 檔案size、檔案name和檔案type) 。

File.__proto__ === Blob   // true           

在web應用中可以通過 type為file的input标簽選擇檔案後生成的 FileList 對象(這裡擷取的是File的一個數組),也可以通過拖拽檔案生成 DataTransfer 對象得到Filelist(通過 DataTransfer.files 擷取)。

File 對象都包含檔案對象的一些屬性,這些屬性都繼承自 Blob 對象,根據MDN的解釋如下:

  • lastModified:引用檔案最後修改日期,為自1970年1月1日0:00以來的毫秒數;
  • lastModifiedDate:引用檔案的最後修改日期;
  • name:引用檔案的檔案名;
  • size:引用檔案的檔案大小;
  • type:檔案的媒體類型(MIME);
  • webkitRelativePath:檔案的路徑或 URL。

FileReader

FileReader 是一個在 JavaScript 中可以用來讀取檔案内容的 API,它允許您讀取 Blob 或 File 對象中的資料,并将其轉換為其他格式,比如文本、二進制資料或資料 URL。

注意:FileReader 和 一些Blob、File操作,隻能讀取檔案不能修改檔案,若想要直接寫入檔案可以使用 a 标簽,指定轉成的文本,進行download,或者可以嘗試一下Chrome的實驗性的window.chooseFileSystemEntries這個api擷取檔案句柄操作(正式穩定版新版或沒有這個Api,了解即可)

  • downloadLink
const blob = new Blob(['{"name": "tong"}']);
const _link = document.createElement("a");
_link.download = 'e.json';
_link.href = URL.createObjectURL(blob);
_link.click();           
  • chooseFileSystemEntries
// 檔案句柄
let handle;
button.addEventListener('click', async (e) => {
  handle = await window.chooseFileSystemEntries();
});
           

1.FileReader 常用屬性:

  • error:表示在讀取檔案時發生的錯誤;
  • result:檔案内容。該屬性僅在讀取操作完成後才有效,資料的格式取決于使用哪個方法來啟動讀取操作。
  • readyState:表示 FileReader 狀态的數字。取值包含0(EMPTY)、1(LOADING)、2(DONE)

2.FileReader 常用 API(傳參是Blob或File):

  • readAsArrayBuffer():讀取資料,result 屬性中儲存的将是被讀取檔案的 ArrayBuffer 資料對象;
  • readAsBinaryString():讀取資料,result 屬性中将包含所讀取檔案的原始二進制資料;
  • readAsDataURL():讀取資料,result 屬性中将包含一個dataURL 格式的 Base64 字元串以表示所讀取檔案的内容。
  • readAsText():讀取資料,result 屬性中将包含一個字元串以表示所讀取的檔案内容。

3. FileReader 常用 事件:

  • abort:該事件在讀取操作被中斷時觸發;
  • error:該事件在讀取操作發生錯誤時觸發;
  • load:該事件在讀取操作完成時觸發;
  • progress:該事件在讀取 Blob 時觸發。

input上傳事件,如下:

<input type="file" id="_input" />


const reader = new FileReader();

_input.onchange = (e) => {
  reader.readAsDataURL(e.target.files[0]);
};

reader.onload = (e) => {
  console.log(e.target.result);
};
           

ArrayBuffer

ArrayBuffer是 JavaScript 中用于表示一段二進制資料的對象。它是在 ECMAScript 6(ES2015)中引入的新類型,允許開發者以更直接的方式處理二進制資料,而無需依賴傳統的字元串和數組。它可以存儲各種資料類型的二進制資料,如整數、浮點數、位元組等。ArrayBuffer 對象的大小在建立時确定,并且不能被改變,是以需要借助如下方式讀寫資料。

  • TypedArray:用來生成記憶體的視圖,通過9個構造函數,可以生成10種資料格式的視圖
Int8Array: 8 位有符号整數(-128 到 127)
Uint8Array: 8 位無符号整數(0 到 255)
Int16Array: 16 位有符号整數(-32,768 到 32,767)
Uint16Array: 16 位無符号整數(0 到 65,535)
Int32Array: 32 位有符号整數(-2^31 到 2^31-1)
Uint32Array: 32 位無符号整數(0 到 2^32-1)
Float32Array: 32 位浮點數(IEEE 754 标準的單精度浮點數)
Float64Array: 64 位浮點數(IEEE 754 标準的雙精度浮點數)
BigInt64Array: 64 位帶符号整數(-2^63 到 2^63-1)(在 ECMAScript 2020 中引入)
BigUint64Array: 64 位無符号整數(0 到 2^64-1)(在 ECMAScript 2020 中引入)           
  • DataViews:用來生成記憶體的視圖,可以自定義格式和位元組序。

包含三個參數:

buffer:一個已經存在的 ArrayBuffer 對象,DataView 對象的資料源。

byteOffset:可選,此 DataView 對象的第一個位元組在 buffer 中的位元組偏移。如果未指定,則預設從第一個位元組開始。

byteLength:可選,此 DataView 對象的位元組長度。如果未指定,這個視圖的長度将比對 buffer 的長度。           

對于ArrayBuffer的讀寫操作,具體如下:

const buffer = new ArrayBuffer(16); // 建立一個大小為 16 位元組的 ArrayBuffer

// 使用DataView 讀寫 ArrayBuffer 資料
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);

view.setInt16(0, 42); // 在位置0寫入16位整數值42
view.setFloat32(2, 3.14); // 在位置2寫入32位浮點數值3.14

const value1 = view.getInt16(0); // 從位置0讀取16位整數值42
const value2 = view.getFloat32(2); // 從位置2讀取32位浮點數值3.14


// 使用 TypedArray 讀寫 ArrayBuffer 資料
const buffer = new ArrayBuffer(8);
const uint8Array = new Uint8Array(buffer);

// 使用 Uint8Array 來設定和擷取二進制資料
uint8Array[0] = 10;
uint8Array[1] = 20;
uint8Array[2] = 30;

console.log(uint8Array[0]); // 輸出 10
console.log(uint8Array[1]); // 輸出 20
console.log(uint8Array[2]); // 輸出 30           

Object URL

Object URL(對象URL)是一個特殊的 URL 格式,用于臨時辨別 JavaScript 中的 Blob、File 和 MediaSource 等二進制資料對象。通過建立 Object URL,您可以在浏覽器中處理這些二進制資料,而無需直接暴露或嵌入這些資料。

Object URL 遵循以下格式:blob:<unique_id>,其中 <unique_id> 是一個唯一辨別符,用于表示一個特定的二進制資料對象。這個 URL 可以被用于在用戶端(通常是在浏覽器中)引用這些二進制資料,例如在 <img> 标簽的 src 屬性、<a> 标簽的 href 屬性、<video> 和 <audio> 标簽的 src 屬性等處。建立 Object URL 的常用方法是使用 URL.createObjectURL() 函數。該函數接受一個 Blob、File 或 MediaSource 對象作為參數,并傳回一個 Object URL 字元串。這個 URL 字元串在 Blob 對象的生命周期内是有效的。

一般情況下,在以下情況下 Object URL 會被自動釋放:

  • 當相關的 JavaScript 代碼執行完成并不再引用 Object URL 時 (垃圾回收釋放)。
  • 當頁面被解除安裝(關閉或導航到其他頁面)時。
  • 當使用 URL.revokeObjectURL() 手動釋放 Object URL 時。

注意:在不需要使用URL時,應該通過 URL.revokeObjectURL() 函數來釋放它,以避免記憶體洩漏。如下:

// 建立一個包含圖檔資料的 Blob 對象
    const imageData = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, /* ... */]);
    const blob = new Blob([imageData], { type: 'image/png' });

    // 建立 Object URL 并将圖檔顯示在 <img> 标簽中
    const imageUrl = URL.createObjectURL(blob);
    const imageElement = document.getElementById('image');
    imageElement.src = imageUrl;

    // 在不需要 Object URL 時釋放它
    // 通常在不需要顯示圖檔或其他二進制資料時調用
    // 這樣可以釋放浏覽器記憶體,避免記憶體洩漏
    // 注意:在實際應用中,請根據需要選擇合适的時機來調用 URL.revokeObjectURL()
    // imageElement.onload = () => {
    //   URL.revokeObjectURL(imageUrl);
    // };           

Base64

Base64 是一種編碼方式,它将二進制資料轉換成隻包含 ASCII 字元的文本表示形式。這種編碼方式通常用于在文本協定中傳輸二進制資料,例如在電子郵件、URL、XML、JSON 等文本格式中嵌入圖檔、音頻、視訊等二進制資料。Base64 編碼基于 64 個可列印的 ASCII 字元(A-Z,a-z,0-9,+ 和 /),以及一個用于填充的字元(通常是等号 =)。編碼規則如下:

  • 将 3 位元組的二進制資料(24 位)劃分為四組,每組 6 位。
  • 将每組 6 位二進制資料轉換為一個可列印字元。
  • 如果原始資料不是 3 位元組的倍數,則會在末尾添加一個或兩個等号(=)字元進行填充。

理論上說:Base64 編碼後的資料大小通常會比原始資料大約 33%,因為每 3 位元組的資料會編碼成 4 個字元。會比blob url消耗更多記憶體,但是在不用的時候會自動從記憶體中清除(通過垃圾回收機制)。

在 JavaScript 中,可以使用 readAsDataURL api 和 canvas 的 toDataURL api 擷取 base64 格式的資料流。

注意:上述方式擷取的是一種根據資料類型生成的base64編碼資料,不是真實一一對應的base64編碼規則,因為添加了一些資料類型頭部,即:base64編碼的資料和 dataUrl 類型的資料不是一個東西。如。`data:image/jpeg;base64,/9j/`,這種的類型 (data: - Data URL 的協定頭部、image/jpeg - MIME 類型,表示資料的媒體類型、base64 - 表示資料是使用 Base64 編碼的。它告訴浏覽器如何解碼資料、/9j/ - Base64 編碼後的資料的起始部分辨別)。

想擷取完整标準的 Base64編碼,可以使用 btoa() 函數将字元串進行 Base64 編碼,以及使用 atob() 函數将 Base64 編碼的字元串解碼為原始字元串。在 Node.js 中,可以使用 Buffer.from() 和 Buffer.toString() 來進行 Base64 編碼和解碼。

  • atob(ascii to binary): 解碼 ,用于将ascii碼解析成binary資料。用于解碼Base64編碼的字元串。
  • btoa(binary to ascii):編碼,用于将binary資料用ascii碼表示。常用于編碼字元串。(不支援Unicode字元編碼),它的編碼範圍實際上是在 0 到 255 的整數值之間,即一個位元組(8 位)的範圍。
const originalString = 'Hello, world!';
const encodedString = btoa(originalString);
const decodedString = atob(encodedString);

console.log(encodedString); // "SGVsbG8sIHdvcmxkIQ=="
console.log(decodedString); // "Hello, world!"           

想要支援其他的Unicode字元編碼,可以使用以下方式:

  • 編碼時,先用encodeURIComponent對字元串進行編碼,再使用btoa進行編碼
  • 解碼時,先用atob對資料進行解碼 ,再用decodeURIComponent對字元串進行解碼
let str_unicode = "中國"
let encode_str_unicode =  btoa(encodeURIComponent(str_unicode));

console.log(encode_str_unicode);  //  JUU0JUI4JUFEJUU1JTlCJUJE
let decode_str_unicode = decodeURIComponent(atob(encode_str_unicode));
console.log(decode_str_unicode);  //  中國           

注意: 上述的decodeURIComponent和encodeURIComponent是一種URI 編碼,一種将特殊字元轉換為可安全傳輸和顯示的形式的編碼方式。在 URI(Uniform Resource Identifier)中,某些字元被保留用于特殊用途,例如作為分隔符或标記符号。如果要在 URI 中包含這些保留字元,就需要将它們轉換為編碼形式,這樣才能正确傳輸和解析。比如我們常見到網頁路徑包含這種參數 `%E4%B8%AD%E5%9B%BD`

格式轉化

以下是一些常用的基本轉化:

1.Blob → ArrayBuffer

let blob = new Blob(['Hello,world'],{type: "text/plain"});
let fileReader = new FileReader();

fileReader.onload = function(event) {
  let result = event.target.result;
  console.log(result);  // ArrayBuffer(11)
};
fileReader.readAsArrayBuffer(blob, 'UTF-8');             

2.Blob → (Base64 | DataURL)

let blob = new Blob(['Hello,world'],{type: "text/plain"});
let fileReader = new FileReader();

fileReader.onload = function(event) {
  let result = event.target.result;
  console.log(result);   //  data:text/plain;base64,SGVsbG8sd29ybGQ=
  let arr = result.split(',');
	let bstr = atob(arr[1]) 
  console.log(bstr)  //   Hello,world
};
fileReader.readAsDataURL(blob, 'UTF-8');             

注意:以上的編碼依舊是不能支援所有Unicode編碼方式,因為blob中的資料有可能是一些高碼位的字元,那麼readAsDataURL生成的dataurl有可能擷取的是亂碼,是以要給定blob生成時的字元編碼方式,此時處理方式如下:

const blob = new Blob([''], { type: 'text/plain;charset=UTF-8' }); // Specify UTF-8 encoding in the Blob
let fileReader = new FileReader();

fileReader.onload = function(event) {
  let result = event.target.result;
  console.log(result);   //  data:text/plain;charset=utf-8;base64,8KCcjg==
  let arr = result.split(',');
	let bstr = atob(arr[1]) 
// Convert binary string to UTF-8 string
const decoder = new TextDecoder('utf-8');
const utf8String = decoder.decode(new Uint8Array(bstr.length).map((_, i) => bstr.charCodeAt(i)));

console.log(utf8String); // ""
};
fileReader.readAsDataURL(blob, 'UTF-8');             

在上述示例中,我們首先使用 atob() 函數将 Base64 編碼的資料轉換為二進制資料(二進制字元串)。然後,我們使用 TextDecoder 對象将二進制資料轉換回原始的 UTF-8 字元串。

注意,TextDecoder 構造函數的參數 'utf-8' 指定了使用 UTF-8 編碼進行解碼。確定在解碼時使用與編碼時相同的字元編碼,以便正确還原原始資料。在我們之前的示例中,我們在建立 Data URL 時指定了 UTF-8 編碼,是以在解碼時也要使用 UTF-8 編碼。

3.Blob → Object URL

let blob = new Blob(['Hello,world']);
const objectUrl = URL.createObjectURL(blob);            

4.Blob → File

let blob = new Blob(['Hello,world']);
let file = new File([blob], "blob", {type: 'text/plain'})  // File{ name: "blob",size:11,type: "text/plain" ... }           

5.ArrayBuffer → Blob

const buffer = new ArrayBuffer(12);
const s = new Uint8Array(buffer);
let u = [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100];
for (let i = 0; i < 12; i++) {
  s[i] = u[i]; 
}
const blob = new Blob([s]);
URL.createObjectURL(blob);    // Hello, World           

6.ArrayBuffer → Base64

這裡編碼方式有兩種,一種是可以通過btoa編碼的,即是上文所說的限制 255 以内的字元。另一種是相容其他的Unicode碼點的轉換方式。(以下是示範需要,實際使用不需要建立其他多餘的資料) 。

//(非高碼位)
const buffer = new ArrayBuffer(12);
const s = new Uint8Array(buffer);
let u = [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100];
for (let i = 0; i < 12; i++) {
  s[i] = u[i];
}
const base64 = btoa(String.fromCodePoint.apply(null,s));
atob(base64)  // 'Hello, World'

// (高碼位)
const encoder = new TextEncoder();
const buffer = encoder.encode("").buffer;  // 這是在高碼位的一個字元
const s = new Uint8Array(buffer);     // 示範需要,轉成buffer

const  decoder = new TextDecoder();
const str = decoder.decode(buffer);
// console.log(str);  // ""  這裡的輸出就是 想要的字元,以下的操作是為了示範Unicode也可以編碼。

// 編碼
let encode_str_unicode =  btoa(encodeURIComponent(str));
// 解碼
// let decode_str_unicode = decodeURIComponent(atob(encode_str_unicode));

console.log(encode_str_unicode);  //  JUYwJUEwJTlDJThF  這裡的轉換得到的高碼位的編碼結果
// console.log(decode_str_unicode);  // ""

           

7. (Base64 | DataURL)→ Blob

const data_url =  "data:text/plain;charset=utf-8;base64,8KCcjg=="; // 這裡是dataurl
let arr = data_url.split(',');
	let bstr = atob(arr[1])  // 這裡就是base64 
// Convert binary string to UTF-8 string
const decoder = new TextDecoder('utf-8');
const utf8String = decoder.decode(new Uint8Array(bstr.length).map((_, i) => bstr.charCodeAt(i)));
console.log(utf8String); // "”
// 這裡必須要給予charset 否則 createObjectURL生成的文本是亂碼
const blob = new Blob([utf8String], { type: 'text/plain;charset=UTF-8' });   
URL.createObjectURL(blob)  // 這裡通路即可得到   字元           

總結: 關于資料流之間的關系以及如何轉換已經基本結束,其他的轉換也可以通過上述的資料轉換以及相關功能得到。關于資料流的一些想法和問題,可以評論區留言。

下一期: 字元編碼工作以及相關解析Api。

繼續閱讀