From: 011netservice@gmail.com
Date: 2022-04-24
Subject: Readme-UploadImagePreviewImmediately.txt
使用 Blob 和 File 相關 Web API 即時呈現上傳圖片檔案
歡迎來信交流.
----------
2021-10-11
完整檔案: 參考 http://www.011.idv.tw/DHelper/UploadImagePreviewImmediately.html
----------
2021-10-11
使用 Blob 和 File 相關 Web API 即時呈現上傳圖片檔案
https://jiepeng.me/2018/04/17/use-blob-and-file-web-api-create-upload-image-preview-immediately
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 可以寫成:
💡 Tips:如果我們需要上傳多個檔案的話可以在 input 再加上 multiple 屬性。
FileList 提供了一個 item 方法,和一個 length 的屬性,item 方法可以得到在陣列內的某個 File,length 則是回傳整個 FileList 的長度。
接下來讓我們使用以上的這幾個 Web API 來建立一個圖片上傳並即時呈現的範例!
範例
首先讓我們建立一個 Container,裡面放置一個上傳的按鈕和一個空的 img:
本範例不使用 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 編碼後的結果了!
完整範例
HTML:
`Blob` and `File` Web API Example
Blob and File Web API Example
JavaScript:
const uploadButton = document.getElementById('upload');
const imgDOM = document.getElementById('upload-image');
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 getFileBase64Encode(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
function handleFiles() {
const fileList = this.files;
const [file] = fileList;
const p1 = createImageFromFile(imgDOM, file);
const p2 = getFileBase64Encode(file);
Promise.all([p1, p2])
.then(result => {
const [img, b64] = result;
// fixed image width, height
img.width = 150;
img.height = 150;
console.log(b64);
});
}
uploadButton.addEventListener('change', handleFiles, false);
CSS:
body {
font-family: 'Source Code Pro';
}
#app {
margin-top: 25px;
}
.file-label {
width: 15rem;
}
input[type=file].hidden {
visibility: hidden;
width: 15rem;
margin-left: -15rem;
}