使用 Blob 和 File 相關 Web API 即時呈現上傳圖片檔案
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"><inputid="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
的物件。
屬性
Property | Description |
---|---|
error | DOMException 類型的物件記錄了讀取資料時發生的錯誤資訊 |
result | 讀取到的資料內容,資料格式則是根據使用的讀取方法 |
readyState | Empty = 0 (尚未讀取資料)、Loading = 1 (讀取資料)、Done = 2 (完成讀取) |
事件處理器
FileReader
繼承自 EventTarget
,所以可以透過 addEventListenter
方法來註冊 Listener。
Handler Name | Description |
---|---|
onboard | 讀取中斷時被觸發 |
onerror | 讀取錯誤時被觸發 |
onload | 讀取完成時被觸發 |
onloadstart | 讀取開始時被觸發 |
onloadprogress | 讀取 Blob 時被觸發 |
onloadend | 讀取結束後被觸發(不管讀取 success 或 failure) |
方法
Method Name | Description |
---|---|
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 編碼後的結果了!