使用 Blob 和 File 相關 Web API 即時呈現上傳圖片檔案

April 17, 2018

Photo by Jeremy Perkins on Unsplash

Blob

Blob(Binary Large Object)類型的物件是一個不可變的原始資料(Immutable Raw Data),類似檔案的二進制資料,通常像是一些圖片、音訊或者是可執行的二進制程式碼也可以被儲存為 Blob。

Blob 的 constructor 接受兩個參數,第一個是實際資料的陣列,第二個則是資料的類型,但這兩個參數不是必須的:

Blob(blobParts[, options])
  • blobParts 是一個陣列類型,可以是以下的元素:

    • Array
    • ArrayBuffer
    • ArrayBufferView
    • Blob
    • DOMString
  • Options 是一個物件類型,可以設定以下兩種屬性:

    • type - 預設為空值,代表會被放入在 Blob 中的 MIME 類型的內容陣列
    • endings - 預設為 transparent

讓我們來建立一個簡單的 Blob:

const url = 'https://www.google.com.tw';
const data = new Blob(url);

console.log(data); // Blob { size: 22, type: "" }

透過 console.log 我們可以看到,我們可以從 Blob 讀取兩個屬性:sizetype

size:二進制資料的大小,單位為 bytes

type:MIME 的格式類型。如果不知道型態,則為空字串

console.log 中我們還看到了另一個 slice 的方法,這裡的 slice 方法我們把它想成是 Array 的 slice,它會回傳一個全新的 Blob 物件,並不會影響到原本的 Blob,我們根據上面的範例來測試:

console.log(data); // Blob { size: 22, type: "" }
const sliceBlob = data.slice(0, 10); // Blob { size: 10, type: "" }
console.log(data); // Blob { size: 22, type: "" }

slice 方法對於內容較大的 Blob 可以將資料以固定大小切分成多塊,例如使用者上傳了一個很大的檔案,伺服器當下可能無法接收這麼大的檔案內容,這時候如果有事先進行分塊並且分批上傳,就可以有效減輕伺服器的負擔。

File

實際上 File 繼承了 Blob,所以除了原來的 sizetype 屬性外,File 還多了以下的屬性:

  • name

  • lastModified

  • lastModifiedDate

  • webkitRelativePath

別忘了原來的 slice 方法也還能繼續使用!

FileList

FileList 型別的物件通常是來自 HTML input 元素的 files 屬性,每個 input 都會有一個 files 的屬性。FileList 其實就是多個 File 的集合,例如我們需要上傳檔案我們 HTML 可以寫成:

<input id="upload-file" type="file" />

💡 Tips:如果我們需要上傳多個檔案的話可以在 input 再加上 multiple 屬性。

FileList 提供了一個 item 方法,和一個 length 的屬性,item 方法可以得到在陣列內的某個 Filelength 則是回傳整個 FileList 的長度。

接下來讓我們使用以上的這幾個 Web API 來建立一個圖片上傳並即時呈現的範例!

範例

首先讓我們建立一個 Container,裡面放置一個上傳的按鈕和一個空的 img

<div id="app">
  <div class="container">
    <div class="row">
      <input
        id="upload"
        type="file"
        accept="image/*"
      />
    </div>
    <div class="row">
      <img id="upload-img" />
    </div>
  </div>
</div>

本範例不使用 jQuery 而是直接使用 JavaScirpt 原生的 API 來操作 DOM 🕹

現在我們建立好了一個簡單的 HTML,接下來我們需要寫 JavaScript 來操作我們的 DOM:

const uploadButton = document.getElementById('upload');
const imgDOM = document.getElementById('upload-img');

function handleFiles() {
  const fileList = this.files;
  console.log(fileList);
}

uploadButton.addEventListener('change', handleFiles, false);

我們在 uploadButton 註冊一個 Event Listenter 來監聽變化,當我們選擇一個檔案時,會觸發 change 事件,這時候執行 handleFiles callback,我們現在裡面簡單的印出 fileList,透過 console.log 你可看到如下的訊息:

▶︎ FileList [ File ]

這時候可以看到 FileList 內有一個 File 的物件,我們可以取出這個物件,並操作它:

const uploadButton = document.getElementById('upload');
const imgDOM = document.getElementById('upload-img');

function handleFiles() {
  const fileList = this.files;
  const [file] = fileList; // 取出 File
}

uploadButton.addEventListener('change', handleFiles, false);

這時候你可能會想 File 所提供的屬性並沒有檔案的 URL 或者是位置,根本沒辦法把圖片印出來,這時候我要透過另一個 URL 的 Web API 來處理。

URL

URL 提供了建立 URL 的方法,你可以透過 new URL() 的方式來建立,這裡我們使用它的靜態方法 createObjectURLcreateObjectURL 接受一個 File 或是 Blob 的物件:

const objectURL = URL.createObjectURL(blob);

所以我們可以繼續完成上面的範例:

const uploadButton = document.getElementById('upload');
const imgDOM = document.getElementById('upload-img');

function createImageFromFile(img, file) {
  return new Promise((resolve, rejfect) => {
    img.src = URL.createObjectURL(file);
    img.onload = () => {
      URL.revokeObjectURL(img.src);
      resolve(img);
    };
    img.onerror = () => reject('Failure to load image.');
  });
}

function handleFiles() {
  const fileList = this.files;
  const [file] = fileList;

  createImageFromFile(imgDOM, file).then(img => console.log(img));
}

uploadButton.addEventListener('change', handleFiles, false);

我建立了一個 createImageFromFile 的函式,它接收一個 imgfile 參數,img 就是該圖片的 DOM 元素,file 則是我們上傳的 File 物件,它回傳一個 Promise,我們可透過回傳的結果再對圖片加以的調整,例如我們可以像是這樣調整圖片的大小:

function handleFiles() {
  const fileList = this.files;
  const [file] = fileList;

  createImageFromFile(imgDOM, file).then(img => {
    img.width = 150;
    img.height = 150;
  });
}

請注意這裡使用到了 URL.revokeObjectURL,當我們得到一個 Blob 作為圖片的來源時(也就是在 onload 的條件下),我們可以呼叫 revokeObjectURL 撤銷對 img.src 的參考,因為 creatObjectURL 只是一個將來源的 src 和媒體(Media)的實例連結起來的一種方法,revokeObjectURL 會留下底層的物件,並在適當的時間處理 Garbage Collection 🚚。

這樣我們就完成一個選擇上傳圖片並即時呈現的簡單效果了!🎉

FileReader

我們前面過有提到了 File 這個物件,物件,我們再稍微介紹他相關的 API 叫做 FileReader

FileReader 物件是可以讓網頁非同步的去讀取在客戶端的檔案,或是原始暫存的資料,所以 FileReader 所接受的參數就是 FileBlob 的物件。

屬性

PropertyDescription
errorDOMException 類型的物件記錄了讀取資料時發生的錯誤資訊
result讀取到的資料內容,資料格式則是根據使用的讀取方法
readyStateEmpty = 0(尚未讀取資料)、Loading = 1(讀取資料)、Done = 2(完成讀取)

事件處理器

FileReader 繼承自 EventTarget,所以可以透過 addEventListenter 方法來註冊 Listener。

Handler NameDescription
onboard讀取中斷時被觸發
onerror讀取錯誤時被觸發
onload讀取完成時被觸發
onloadstart讀取開始時被觸發
onloadprogress讀取 Blob 時被觸發
onloadend讀取結束後被觸發(不管讀取 success 或 failure)

方法

Method NameDescription
readAsText讀取 Blob 完成後,result 將會是文字表示讀入資料的內容
readAsDataURL讀取 Blob 完成後,result 將會是 Base64 編碼表示讀入資料的內容
readAsBinaryString讀取 Blob 完成後,result 將會是原始二進制資料表示讀入資料的內容
readAsBufferArray讀取 Blob 完成後,result 將會是 ArrayBuffer 物件來表示讀入資料的內容
abort中斷讀取,此方法回傳後屬性 readyStateDONE

以上是 FileReader 的相關屬性和方法,我們再來改寫上面的範例,使用 FileReader 來得到 Base64 編碼的結果,這裡我們需要用到 readAsDataURL 方法。

改寫範例

首先我們先新增一個 getFileBase64Encode 方法:

function getFileBase64Encode(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.readAsDataURL(blob);
    reader.onload = () => resolve(reader.result);
    reader.onerror = error => reject(error);
  });
}

這裡使用了 Promise,當檔案成功讀取後,使用 resovle 來回傳 reader 的結果,若是讀取中發生錯誤,我們透過 reject 來拋出錯誤。

接著在 handeFiles 加入這個函式:

function handleFiles() {
  const fileList = this.files;
  const [file] = fileList;

  createImageFromFile(imgDOM, file).then(img => {
    img.width = 150;
    img.height = 150;
  });

  getFileBase64Encode(file).then(b64 => console.log(b64));
}

接著在 console.log 就可以看到圖片 Base64 編碼後的結果了!

完整範例

JSFiddle

Reference