假設一個應用場景:由于某些特殊原因從服務端請求到圖片路徑(圖片被存儲在服務器上),要求通過該路徑獲取對應圖片的 base64 dataURL。在這個場景中,我們首先推斷該圖片路徑是可訪問的,同時還需要一種將圖片轉換到 dataURL 的方法。我們如何實現它呢?
dataURL
先大致回顧下正統的 dataURL 的語法,這有助于我們檢驗轉換后的內容是否正確。一個完整的 dataURI 應該是這樣的:
data:[<mediatype>][;base64],<data>
其中mediatype
聲明了文件類型,遵循MIME規則,如“image/png
”、“text/plain
”;之后是編碼類型,這里我們只涉及 base64;緊接著就是文件編碼后的內容了。我們常常在 HTML 里看到img
標簽的src
會這樣寫:
src="data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7"
這個img
引用的就是以 base64 編碼的 dataURL 了,只要瀏覽器支持,就可以被解碼成聲明格式的圖片并渲染出來。
.toDataURL()
這是一個功能函數,FileReader
對象也有類似的方法,比如.readAsDataURL()
,然而它只接受file
或blob
類型,而這兩種類型一般只能通過<input[type=file]>
元素的files
屬性獲取,或者用Blob()
構造函數手工創建一個新的對象。尷尬的是我們當前只有圖片路徑,受制于瀏覽器的安全策略,<input[type=file]>
的files
屬性是只讀的,而Blob()
構造函數只接受文件內容,兩種方式都無法通過圖片路徑直接獲取。上文中假設的應用場景迫使我們必先考慮如何通過路徑獲取到圖片內容。<img>
是可以的,并且可以被繪制到<canvas>
中,而<canvas>
正巧擁有.toDataURL()
方法。
萬事具備,我們只需要把<img>
獲取到的圖片放到<canvas>
里再通過.toDataURL()
方法轉化下,就可以得到以 base64 編碼的 dataURL。來看這個方法的語法:
canvas.toDataURL([type, encoderOptions]);
canvas 是 DOM 元素<canvas>
對象;參數type
指定圖片類型,如果指定的類型不被支持則以默認值image/png
替代;encoderOptions
可以為image/jpeg
或image/webp
類型的圖片設置圖片質量,取值0-1
,超出則以默認值0.92
替代。
需要注意的是,圖片加載是異步的,在轉換成 dataURL 前必須先確保圖片成功加載到,否則讓 canvas 即刻執行繪制可能失敗,從而導致轉換 dataURL 失敗。于是.toDataURL()
方法應該寫在<img>
的onload
事件中,以確保 canvas 的繪制工作在圖片下載完成后開始。另一個問題是<img>
圖片渲染到<canvas>
上也需要一個過程,好在.drawImage()
方法是同步的,只有在 canvas 繪制完成后才會執行后續如.toDataURL()
的操作。現在就來實現一個功能函數:
function getBase64(url){
// 通過構造函數來創建的 img 實例
// 在賦予 src 值后就會立刻下載圖片
// 相比 createElement() 創建 <img> 省去了 append(),也就避免了文檔冗余和污染
let dataURL = ''
let img = new Image();
img.src = url;
img.onload = () => { // 要先確保圖片完整獲取到,這是個異步處理
let canvas = document.createElement('canvas'); // 創建canvas元素
let [width,height] = [img.width,img.height]; // 確保canvas的尺寸和圖片一樣
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height); // 將圖片繪制到canvas中
dataURL = canvas.toDataURL('image/jpeg'); // 轉換圖片為dataURL
}
}
一個可供隨時調用的轉換函數完成了,它會在圖片被加載后返回一整個 dataURL 字符串。
完善
onload
事件確保了轉換任務在圖片加載后執行,卻又帶來了新問題——dataURL 只有在圖片加載完成后才會返回,我們是無法精準確定圖片完成加載的時間的。如果后續要對 dataURL 做相關處理(比如傳遞到其他服務器)的話,添加一個回調是必要的,這能確保后續處理任務在成功得到 dataURL 之后執行,我們修改一下getBase64()
:
function getBase64(url, callback){ //添加一個回調參數
...
img.onload = () => {
...
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
dataURL=canvas.toDataURL('image/jpeg');
callback&&callback(dataURL); //調用回調函數
}
}
在執行時添加回調:
let imgURL = '//upload.jianshu.io/users/upload_avatars/555630/fdd1b798e6b0.jpg';
getBase64(imgURL, dataURL => {
console.log(dataURL)
});
就是這樣。如果不考慮兼容性的話,或許我們可以用 promise 和 generator 來實現,再添加一些錯誤處理就更完美了。