iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
JavaScript

Don't make JavaScript Just Surpise系列 第 29

JavaScript 裡的二進位與關於檔案的那些事(ArrayBuffer, Blob and File)

  • 分享至 

  • xImage
  •  

上傳檔案,生成檔案,顯示檔案內容,都會是 JS 開發者在實際開發應用的時候常見的需求。
雖然 JS 中處理檔案的物件 File 不是由 ECMAScript 定義的,而是定義於 Web API 標準中(W3C),但由於檔案的泛用性,所以會在這篇介紹 JS 中的檔案物件該如何被使用。
附帶一提,Web API 主要是涵蓋 JS 中該如何和瀏覽器的各種資源與介面互動的標準,是現代網頁的開發基礎。

我們能拿到一個 File 物件的最簡單的方法之一就是網頁上 <input type="file"/> 的輸入標籤,他會在前端提供使用者上傳選擇檔案的介面,當使用者確定上傳,載入完畢後,就會得到一個 File 物件。

File 物件內部是怎麼儲存這樣的檔案內容跟資訊的?我們得先了解一些額外的資料結構和知識。

二進位(Binary)

對電腦來說,所有資料最終都由 0 和 1 的組合表示。
這種數學計數方法稱作二進位,到 2 就往下一個位進,永遠只有 0 和 1。
檔案也不例外,底層就是這樣存的。

那如果以一個純文字檔案,0 和 1 要怎麼被表示和讀取?

編碼系統(Encoding)

答案是:我們會透過特定的編碼方式將每個字元對應到特定的二進位表示。

編碼(Encoding) 的定義是指將資料或訊息轉換成特定格式的過程,目的是資料的儲存、傳輸或處理。
編碼不單包含圖片,也有文字編碼(UTF-8),影像編碼(JPEG、PNG),壓縮編碼(Base64)。

最早的編碼系統是 ASCII(American Standard Code for Information Interchange)。
早期的程式設計是從美國發展起來的,那編碼系統自然是由美國推出,這個編碼系統做的事情就是把一些符號和英文字母 1 : 1 對應到數字,數字自然可以用二進制轉為 0 和 1 的組合,讓電腦得以儲存與識別。
這個最陽春的編碼系統只涵蓋了 128 個字元,且並不支援除了英文以外的語言。

早期因為電子郵件的協議(如 SMTP)只允許文字內容的 ASCII 傳輸,不支援二進位的其他資料類型,如圖片或音訊。
針對這個情境,我們需要一種能將圖片、音訊轉為 ASCII 表達的方式,沒錯,再一種編碼:Base64 為此而生,從此有了將二進位資料轉為 ASCII 編碼的方式。
後來網頁開始發達,網頁也是以文字文件方式儲存的檔案,所以圖片也能以 Base64 的編碼解析後被顯示在網頁上。但 Base64 有編碼後會讓體積增大約 33 % 的缺點,所以針對較大的圖片或其他檔案類型,通常不會使用 Base64,但對小型圖片儲存(減少從外部載入的時間)或有傳輸需求,他都可能是一個選項。

再提另一個主流編碼:在編碼的發展史中,中間有很多的區域性編碼出現,或是針對 ASCII 做擴充,但下個到現在仍在用的通用且主流編碼是:Unicode。
Unicode 稱作萬國碼,Uni 是 Universal 的感覺,目的是讓全球有一個統一的編碼系統,且每個編碼都對應到某一個特定的字元。
如果各個國家都有自己的編碼系統,可以想像可某個編碼 A5678 代表中文的 「中」,但在日本,也許 A5678 被用來代表 「文」。在這樣的情境下,為了有一個全世界通用,一個編碼只代表唯一一個字元的情況,Unicode 規範誕生了,至今他幾乎包含了全世界大部分書寫系統裡的文字、符號和其他任何字元。

我們常在網頁開頭看的 UTF-8(Unicode Transformation Format - 8-bit) 就是一種實踐了 Unicode 規範的編碼方式,其他還有如 UTF-16、UTF-24 都是遵循 Unicode 規範實踐的編碼方式,只是彼此提供針對字元的操作、儲存方式、或傳輸的相容性可能有些不同。
網頁中最常使用的還是 UTF-8。

瀏覽器中的 Base64 範例

在瀏覽器中,因為 HTML 是一個純文字的檔案,我們嵌入圖片時,如果希望在文件直接包含圖片內容資訊,就必須依賴能將非文字數據轉為純文字字串的方式:Base64 就是其中一種常用的處理方式。
例如你複製下面的碼到 jsfiddle,他就能產出一個圖片。

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABQBAMAAAAdJ83cAAAAMFBMVEUAAAAAAAAAAAAAAAAAAAD/5A8AAAAhGgTSpAp4XgWnggj9yA1LOwj8/Py6urp1dXZPBd+QAAAAB3RSTlMAGzxrsP7+oefQvQAAAAlwSFlzAAALEwAACxMBAJqcGAAABSFJREFUeJyllz1sW1UUgB3H8txKKIgtqFKnDKaNqCoxVEWKCmK9+OdmcINzbcePRAjsKBBg4blp0g3skpARh/yt10m53UJsItw1LLjdOpGMniqVc859/8/PCHEG+9nvfT4/9/w5FhsiV6/dvHn1yrA7wyU+LVbPNjd7tcIN/HjjX4GxaWNrg6Hwte3CO7GxwoiHJ/FlfOZHybgiYey4e3diBDI2h4RhMuWKZA+EGKFFpICQXHlFHo1GCvEgoTJCiLlIIi5EUTq/br3VAdm+E+kL3G1wcgD8N/GFlIhGc3IEIkyI0/FhB66KZweStfG7aubzaMOEKDP+i7DF2KW3eZmOMC1J93+viaCUlGwOR4zQszbCVTY/VMlaFFLmEWrKuSjEgNBlh3iTOI1EBCDytzByi0cjeKjpySAxVhmBmJg5oQAkWmo2EmlhSvwQRN6DQEYip0Mtg9hHa1ngQywbbyi5E4kIzGt54keS8F00IapUByFXou2yLJv1O3OPj7JLn0zuju9UqkqGM1jAVw/1VQMtux/wPmM99sglKnUh2FN9icj3Pu9NcqXSgWeO3DiBrUp2bGfkz4GAYcmacFtxx2cGCJdUy0X0v+1Fpsh7gyEimxZSwu+4Sjv+p719/TZH/RZih67MyLA6fcA0m015kA+5FLZh0vHFRITpSGKa5bzIPU5nP99xQkSWAYI9GaUaPJiSFWP4xXX/0TwWLpLxIhXnWKIED0bl/zOS8SPRVfy/tASQqGpZ7KOch923gjxEli5RXrqInQF4lK8v+/0hBWAJZn8uj+Mh5STMN/3+5etoXzBhsJdft8fgVGRRLv6t3yktwbAZYc30Nzklv1gK+9J/gW+U/B8T8olGrBIT/SH+9593rBKrAyJsBNorHsziXyHilShe9Ae6kB97kXiVCvZi8G0QQb3P+rpdrKYQsbeACpbS4gtxEUSeDeBlGSeMyq34kNvY+i4GYeTTc3yl1pfem8RBbyMTCtrEpRDPg0iRAkatvz1LiJ1oiYYu8hch/0kv7Sa9NK0TTgOEkMB8KYZD9vWApjhIpU3rhJPOH+CgFkt/hpDFc50tKt34DtqqB5mBeThL94POvNTOyx3sll7k1lfwbdMYhBBhKZEldkKIUzRv9biKmJbak/Sp/Ix8cZBku6HszhiwjLY52eTYLbxaEi38Md6JMEvlKjwLh5/wIPFqnRaCUF3+wfXCaNIUB8TtsqUcqpFBRhMqU+I0Xr2GxabYDqqRGZ9tP2lCwj0a4qBl0kGSZqZM8eePHEUrprUCZ9GCvNbiGhavyvYpPcH4fmdZFFc3N5xFviat6eLTEvuIy5ppPcK4pM3XWpZ3IJMljb2kEJ5RlmypnOHd922RTykwJxbiEriRyaOFMCOzhnTm0bs+ZByWG1kPMexJkcxtk0FOG7OQCldsp+z7o8B4s9iiQH5BlggfktDb+4Piges3XxeGDnSOIpXw/81I6K7AntRWDpgWONYvtVJYGPLa+/t+hNJD8sNasbe1v7vXFSsblmuwFBXIFbfCYtbK39IOrG9ud/HwHROx/VIXE6kgQpZzsOlh98w5fKU3jjwZEgsiwpBM/QomrbpBUEqvQoXYhNORKXxXrltFWIP02lJeAI6ri7dSXleuTYvl1V7vbG9vd3dNMeYDQBbYMawZc64rb4jevslskSok6VP4f3Z8KJyDHFuQoZ/1iezS+TO+Vyu8rf02RzyOkp23LqCSuoW7MRqUIwX6pHMN/5o3a+//A3CXnB9vMVgjAAAAAElFTkSuQmCC"/>

瀏覽器會首先檢查最前面的 data:image/png; 協定,來知道該如何顯示這樣的內容,接著檢視後面 Base64 編碼內容,再將編碼的文字轉換回原始的二進位資料(解碼,Decoding),最後渲染為圖像顯示在畫面上。

二進位儲存物件

如上所說,檔案的底層都是二進位資料儲存。
而在 JS 中定義來處理二進位的底層資料結構主要有以下兩種:ArrayBufferTypedArray

ArrayBuffer 用於低層級的二進位處理,他只儲存內容,不提供任操作方法,固定長度,宣告後不可變。
也因為這樣,對其內容的存取我們依賴 TypedArray 這個資料結構。

TypedArray 是一個抽象結構,也就是說他沒有實例,實際上他的實例依 MDN 所列,共有 12 種。
這 12 種物件的差別在於儲存的值的範圍,物件大小,有無符號等,依據二進位資料的性質,決定使用的對象。
一般圖片或 Canvas,我們會使用 Uint8Array 來進行表示,因為剛好 Uint8Array 的範圍是 0 - 255,對應顏色表示方法的 RGB,且不會有負值需求。

下面用一個例子展示這兩個資料結構的交互作用。
假設我們有一個 canvas。

<canvas id="canvas" width="20" height="20"></canvas>

這是對應的 JS。

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 20, 20);

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;//從 canvas 讀取圖像資料

//依據轉為二進制資料後的長度來宣告 ArrayBuffer
const buffer = new ArrayBuffer(data.length);
//以該大小的 ArrayBuffer 來建構一個對應的 typedArray
const typedArray = new Uint8Array(buffer);
//塞入內容到 typedArray 中
typedArray.set(data);

console.log('Original Data:', data);//[object Uint8ClampedArray]
console.log('ArrayBuffer Display as String:', buffer);//[object ArrayBuffer]
console.log('ArrayBuffer Length:', buffer.byteLength);//"ArrayBuffer Length:", 1600
console.log('TypedArray Length:', typedArray.length);//"TypedArray Length:", 1600
console.log('TypedArray Data:', typedArray);//[object Uint8Array]

因為 ArrayBuffer 算相對底層的二進位儲存方式,在後續操作如果要轉為其他類型或解碼上,都會是相對通用的類型。

Blob(Binary Large Object)

處理二進位資料的還有一個結構:Blob(Binary Large Object)。
這個資料結構和上面 ArrayBuffer 和 TypedArray 最不一樣的地方在於,Blob 允許創建一個在瀏覽器中可用的 URL,使得瀏覽器可以直接用該 URL 來顯示或下載 Blob 中的資料。

而 Blob 的初始化方式即是由 ArrayBuffer 創建而來。
所以關係是像這樣

ArrayBuffer -創建-> Blob -產出-> Browser Usable URL
^
|
填入資料
|
TypedArray

Blob 創建後不可直接更改內容,需要時需要重新創建一個新的 Blob。
Blob 建構時可接收兩個參數 array,option,第二個參數可選。
array 就是建構用的 ArrayBuffer,也能夠傳入 TypedArray,DataView(另一種提供 ArrayBuffer 寫入能力的結構),Blob,DOM 等。
option 包含兩個選項:typeendings,前者用於指定該 Blob 的 MIME 類型,說明是什麼文件,後者用於換行符號的指定,會影響後續對 Blob 資料傳入與讀取。

有做過網頁開發的人應該看過像這樣的程式碼:

const imgUrl = "https://...";
fetch(imgUrl)
  .then(response => response.blob())
  .then(blob => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'image.png';
    a.click();
    URL.revokeObjectURL(url);
  });

比如需要讓使用者在操作瀏覽器時下載一個圖片,找的範例程式碼大概跟這樣八九不離十,講解完 ArrayBuffer 和 Blob 的概念後,相信比較理解這段程式碼會什麼可以作用了吧?

File Object

終於到了 File Object 本身。

其實,File 是從 Blob 擴充而來,他的基底就是 Blob,Blob 有的他都有,可以說是一種特化的 Blob。
Blob 算更廣義的二進位操作資料結構,只表示一個不可變的二進位資料內容。
而 File 上面帶有如 name,size,type,lastModified 等等屬於檔案的屬性,通常 File 物件會是從使用者介面如開頭提到的 <input type="file"/> 獲取的,而 Blob 可以透過二進位資料直接建立。
像 type 屬性雖然 Blob 也有,但可於創建時自己指定,而 File 通常是從檔案系統(File System) 產生物件的時候賦予的,會更貼近物件的實際類別(更可信)。

如果單單討論檔案的處理,那我們更會傾向使用 File 而不用 Blob,因為更多直接適用於檔案的方法或屬性,也有對應的其他類別會需求 File 物件來執行。

從 Input 中獲取檔案:FileReader

雖然說可以從 <input type="file"/> 獲得一個 File 物件,但我們該怎麼讀取 File 物件的內容?
有個叫 FileReader 的物件可以幫助我們做到這件事。
這個物件可以讀取 Blob 或是 File 物件的內容供我們所用。

FileReader 主要提供三個方法來讀取內容(不計有個目前已被棄用的 readAsBinaryString):

  1. readAsText():讀取純文字內容的時候
  2. readAsDataURL():讀取如圖片並顯示的時候
  3. readAsArrayBuffer():讀取為 ArrayBuffer 來維持二進位的資料格式,適用於想直接處理二進位資料時

這四個方法流程都一樣,會先以指定的方式嘗試讀取檔案內容,這個讀取是異步發生的,所以我們可以使用 Promise 來包住讀取行為。
而當讀取完成後,物件的 loadend 事件會被觸發,通常會在這步去對讀出來的內容做後續處理(如顯示在網頁上)。

function printFile(file) {
  const reader = new FileReader();
  reader.onload = (evt) => {
    console.log(evt.target.result);
  };
  reader.readAsText(file);
}

這是一段 MDN 上的範例程式碼,可以看到我們使用了 onload 事件來綁定當 reader 載入內容的時候要執行的事情,loadend 事件觸發就會執行 onload 裡面的內容。

這個物件是其中一種目前瀏覽器中主流用於讀取上傳檔案後的本地端處理。

無法依靠內建方式處理的檔案類型

如上所說,主要目前能照顧到的就是純文件、純圖片,其他更複雜的格式,如 PDF,並不被目前的瀏覽器 JS 標準直接支援,多需依賴外部的函式庫,如 PDF.js。
差異在於這些函式庫中有按照該文件類型的規範方實作對編碼內容的讀取,知道該如何分段,各段內容又代表什麼。
要處理複雜的檔案類型,通常還是要依賴外部函式庫,但像儲存、文件傳輸與處理,還是會很常看到這篇提到的那些底層二進位處理物件,如 Blob、TypedArray 等等。

相互轉換與儲存

上面介紹了 JS 中用於處理檔案和二進位的各種資料結構,由於底層是二進位,可以想見,其實這些資料結構都能夠透過某種方式進行互相轉換。
希望透過這篇全面的介紹 JS 中檔案與二進位,在面對需要在網頁上處理檔案的傳輸、儲存、顯示等等問題時,不再是只能複製貼上,而懂的為什麼要用那樣的寫法來解決問題,也能理解型別的選擇。


上一篇
垃圾回收機制(Garbage Collection)
下一篇
JavaScript 裡的事件(Event)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言