← index

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

Use `Blob` and file web API create instant image preview.

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 讀取兩個屬性:size、type

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,所以除了原來的 size 和 type 屬性外,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 方法可以得到在陣列內的某個 File,length 則是回傳整個 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() 的方式來建立,這裡我們使用它的靜態方法 createObjectURL。createObjectURL 接受一個 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 的函式,它接收一個 img 和 file 參數,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 所接受的參數就是 File 和 Blob 的物件。

屬性

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中斷讀取,此方法回傳後屬性 readyState 為 DONE

以上是 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