1. 前言
如果您只是爲了copy代碼實現功能,建議您不要看這篇博客了。如果您是copy了代碼實現了功能,想回來瞭解具體的實現流程、實現原理以及部分科普,我覺得本篇博客能給你帶來不小的收穫。
2. 任務背景
最近的項目有一個技術場景,簡單來說就是用戶需要上傳圖片至服務器。就是這麼一個簡單的技術場景,但是用戶是不可控的,他們可能上傳的圖片會是幾M甚至十幾M的大小,如此大的圖片必然會導致上傳時間過長,用戶感知不好,服務器壓力大等不良影響。
作爲組裏唯一一個前端,老大就給了我一個任務,在用戶上傳圖片前把圖片壓縮了再上傳。那麼本篇博客主要講解的就是圖片壓縮中我是如何進行壓縮的,之後的博客會講解如何將壓縮的圖片進行上傳。
3. 流程說明
本文實現的功能流程如下:
- 用戶通過input框選擇圖片
- 使用FileReader進行圖片預覽
- 將圖片繪製到canvas畫布上
- 使用canvas畫布的能力進行圖片壓縮
- 將壓縮後的Base64(DataURL)格式的數據轉換成Blob對象進行上傳
簡要流程圖如下:
FileReader
?我知道
canvas
是畫布,但是具體是怎麼樣用它進行圖片壓縮的呢?Base64我知道但是什麼是
DataURL格式的數據呢?
Blob對象又是什麼?
同學們,不着急,對於這些我都會進行一一講解,因爲這也是作爲一個小白的我初次見到這些陌生名詞時的疑惑。
4. 效果預覽
5.1 FileReader是什麼
我們先來看看MDN上對它的解釋:FileReader
對象允許Web應用程序異步讀取存儲在用戶計算機上的文件(或原始數據緩衝區)的內容,使用File
或 Blob
對象指定要讀取的文件或數據。
通俗來講,就是這個對象是用來讀取File
對象或Blob
對象的。File
對象就是<input type="file">
獲取到的對象,而Blob
(二進制)對象在本文的第5點有講解。
5.2 FileReader的鉤子與方法
作爲一個js原生的用於讀取文件的對象,FileReader
本身就有較爲完整的鉤子函數以及一些實例方法,但是本文主要介紹圖片壓縮,所以在這裏只重點講本文使用到的1個鉤子函數和1個實例方法,對其它的鉤子和方法都不做詳細介紹。
FileReader.onload
:處理load
事件。即該鉤子在讀取操作完成時觸發,通過該鉤子函數可以完成例如讀取完圖片後進行預覽的操作,或讀取完圖片後對圖片內容進行二次處理等操作。
FileReader.readAsDataURL
:讀取方法,並且讀取完成後,result
屬性將返回 Data URL
格式(Base64 編碼)的字符串,代表圖片內容。
除了用到的這個鉤子和這個實例方法外,FileReader
對象還有onabort
、onerror
、onloadstart
、onloadend
、onprogress
等鉤子;也有abort()
、readAsArrayBuffer
、readAsBinaryString
等實例方法,在次就不過多描述。
5.3 FileReader在圖片壓縮中的作用
在onload
這個鉤子對上傳的圖片實現了預覽,並且進行了圖片壓縮處理。通過readAsDataURL()
方法進行了文件的讀取,並且通過result
屬性拿到了圖片的Base64(DataURL)
格式的數據,然後通過該數據實現了圖片預覽的功能。有的同學看到這裏是不是有點好奇,爲什麼拿到了這個Base64(DataURL)
格式的數據就能直接展示處圖片了呢?不要緊,往下看,我會在後文中解釋這個DataURL
格式的神奇。
FileReader
部分代碼如下:
var reader = new FileReader(); //讀取文件的對象
reader.readAsDataURL(file); //對文件讀取,讀取完成後會將內容以base64的形式賦值給result屬性
reader.onload = function (e) { //讀取完成的鉤子
console.log("原始二進制字符串:",this.result.toString());
const img = new Image();
const quality = 0.2; // 圖像質量
const canvas = document.createElement('canvas');
const drawer = canvas.getContext('2d');
img.src = this.result;
console.log("FileReader對象:",this);
//圖片預覽
var picDom = $(item.item).find("img");
picDom.attr('src', this.result); //圖片鏈接(base64)
//圖片壓縮轉碼
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
drawer.drawImage(img, 0, 0, canvas.width, canvas.height);
convertBase64UrlToBlob(canvas.toDataURL(file.type, quality), callback);
}
}
}
6. canvas(圖片壓縮的核心)
canvas
元素衆所周知是畫布,那麼canvas
在圖片壓縮中起到了什麼作用?實現圖片壓縮的核心內容主要是使用到了canvas
的什麼方法?
6.1 Canvas在圖片壓縮中的作用
實現圖片壓縮最核心的地方就在canvas
這裏,我們先使用CanvasRenderingContext2D.drawImage()
方法將選中的圖片文件在畫布上繪製出來,再使用Canvas.toDataURL()
將畫布上的圖片信息轉換成base64(DataURL)格式的數據。有同學會問,那麼是在哪兒實現的壓縮?其實壓縮的核心就在Canvas.toDataURL()
方法的quality
參數上了,下面我們會具體介紹本文中使用到的2個canvas
畫布上的方法。
6.2 CanvasRenderingContext2D.drawImage()
語法如下:
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
複製代碼
這個方法是CanvasRenderingContext2D
上的繪製圖片的方法,如果有朋友想了解CanvasRenderingContext2D
和canvas
之間的聯繫和區別的話可以自行了解,這裏不過多贅述。
首先介紹一下該方法所接受的9個參數:
image
:Object;繪製在Canvas上的元素,可以是各類Canvas圖片資源),如圖片,SVG圖像,Canvas元素本身等。
dx
:Number;在Canvas畫布上規劃一片區域用來放置圖片,dx就是這片區域的左上角橫座標。
dy
:Nmuber;在Canvas畫布上規劃一片區域用來放置圖片,dy就是這片區域的左上角縱座標。
dWidth
:Number;在Canvas畫布上規劃一片區域用來放置圖片,dWidth就是這片區域的寬度。
dHeight
:Number;在在Canvas畫布上規劃一片區域用來放置圖片,dHeight就是這片區域的高度。
sx
:Number;表示圖片元素繪製在Canvas畫布上起始橫座標。
sy
:Number;表示圖片元素繪製在Canvas畫布上起始縱座標。
sWidth
:Number;表示圖片元素從座標點開始算,多大的寬度內容繪製Canvas畫布上。
sHeight
;表示圖片元素從座標點開始算,多大的高度內容繪製Canvas畫布上。
很多同學看到這裏這麼多個參數是不是有點懵逼了?不用慌,我也很懵逼,其實我對canvas也不是很瞭解,不過沒關係我們直接看看本文中使用到這個方法時我們傳遞了什麼參數
//代碼
//先創建canvas畫布,再獲取canvas畫布上的2d繪圖環境,通過這個2d繪圖環境纔可使用繪製API
const canvas = document.createElement('canvas');
//返回一個在畫布上繪製2d圖的環境對象,該對象上包含有canvas繪製2d圖形的API
const drawer = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
drawer.drawImage(img, 0, 0, canvas.width, canvas.height);
//實際傳遞的參數
drawer.drawImage(image, dx, dy, dWidth, dHeight)
結合上面列舉的9個參數,我們可以知道我們實際上只使用到了5個參數。我們傳遞了1個圖片,緊接着我們定義了canvas上的起始區域座標點以及這個canvas上放置圖片的區域的寬高。
實質上,我們設置的canvas放置圖片區域的寬高大小跟圖片本身是一模一樣的,因爲在本文中,使用該方法的目的不在於在canvas上展示一張圖片給用戶看,而是在於在canvas上繪製出這張圖片,這樣我們才能使用接下來的的這個Canvas.toDataURl()
方法
6.3 Canvas.toDataURl()
咚!咚!咚!敲重點了,這個方法纔是本文中實現圖片壓縮的核心。
語法如下:
Canvas.toDataURl()
方法可以將canvas畫布上的信息轉換爲base64(DataURL)格式的圖像信息,純字符的圖片表示形式。該方法接收2個參數:
mimeType
(可選):String;表示需要轉換的圖像的mimeType類型。默認值是image/png,還可以是image/jpeg,甚至image/webp(前提瀏覽器支持)等。
quailty
(可選):Number;quality表示轉換的圖片質量。範圍是0到1。此參數要想有效,圖片的mimeType需要是image/jpeg或者image/webp,其他mimeType值無效。默認壓縮質量是0.92。
到這裏,很多同學就可以知道了,前端圖片壓縮的核心方法,是不是就在這個方法的quailty
參數上面呢?好了,讓我們看看本文使用到該方法的地方:
const canvas = document.createElement('canvas');
const quality = 0.2; //設置壓縮比例
canvas.toDataURL(file.type, quality)
可以看到,本文中設置了壓縮質量爲0.2,需要注意的是不是說壓縮質量設置爲0.2實際壓縮效果就爲5倍壓縮(在本文中,壓縮質量設置成了0.2但實際壓縮效果確實7-9倍),簡單的說,當到達了這一步以後,其實圖片已經完成了壓縮,我們已經可以直接拿着返回的base64(DataURL)格式的數據去渲染圖片,但是,如果你的目的是將圖片先進行壓縮,壓縮後再上傳給服務器,並且服務器只接受二進制的圖片信息的話,那就得好好考慮怎麼將base64轉換成二進制Blob對象了,關於Blob,不要着急,我會在下一篇上傳篇中對它進行科普。
小tips:該方法爲同步方法,如果需要轉換的Canvas尺寸很大,則會阻塞腳本的運行,因此需要注意控制Canvas的尺寸。
6.4 Canvas.toBlob()
語法如下:
canvas.toBlob(callback, mimeType, quality)
如果看了6.3小節的同學應該會覺得,這2個方法長得差不多,參數也差不多,那麼它們的效果是否也是差不多的呢?
當然,該方法的作用是將canvas畫布上的信息轉換爲Blob對象。該方法接收的參數基本與6.3的方法相同,區別在於,該方法多接受一個參數,該參數爲:
callback
:Function;toBlob()方法執行成功後的回調方法,支持一個參數,表示當前轉換的Blob對象。
6.3小節的最後說過,toDataURL()
方法是同步方法,那麼我們toBlob()
與此不同,它是一個異步的方法,所以該方法會多接受一個參數callback
,該參數就是toBlob()
的回調函數。
好了,既然本文最終目的是將file壓縮後,再轉換成Blob對象上傳至後端,那麼爲什麼不直接使用toBlob()
方法,而是使用toDataURL()
方法呢?對於這點的解釋我會在下一篇上傳篇中進行詳解,各位同學可以持續關注我的博客。
7.DataUrl格式
不知道有心的同學發現了嗎?本文中多次提到了DataURL
格式的數據,那究竟什麼纔是DataURL
格式的數據呢?
在下面對DataURL
展開了解之前,我們可以先來複習一下常見的img
標籤的src是什麼樣的?
那麼src除了這種賦值方式之外,還有什麼形式能夠展示圖片嗎?
當然有,那就是我們的DataURL
,詳見下圖:
DataURL
在實際中有什麼用處呢?它的定義是什麼?什麼場景下需要用到它?帶着這個疑問看下去吧。
7.1 DataURL格式的定義
先來看看我從網上找到的比較官方的定義:DataURL
是由RFC2397定義的一種把小文件直接嵌入文檔的方案。格式如下:
其實整體可以視爲三部分,即聲明:參數+數據,逗號左邊的是各種參數,右邊的是數據。舉個例子:
我們將例子與格式一一對照來看
MIME type
:表示數據呈現的格式,簡單來說就是這個數據的類型是什麼?是png還是jpg甚至是html,結合實例來看,在實例中我們的這段數據它的類型就是一個圖片,而且是個jpg格式的圖片。
character
:字符集;這個是可選項,默認爲charset=US-ASCII
,如果指定的是圖片的話,就不再使用字符集了。在實例中,我們的這段數據代表的就是一張圖片,隨意是沒有這個字段的。
base64
:這一部分將表明數據編碼方式,當然我們可以不使用base64的編碼方式,那樣我們將使用標準的URL編碼方式。在本實例中,我們的圖片就是採取base64的編碼方式。
enconded data
:這就是實際的數據了,在實例中,這就是這張圖片的base64編碼。
好了,DataURL
的定義講完了,那麼DataURL
只能在圖片裏使用嗎?當然不是,它能表示的東西有很多很多,比如你們可以複製下面的這段代碼到瀏覽器地址欄中粘貼看一看
data:text/html;charset=UTF-8,<html><body><p>歡迎看劉偉C的博客</p></body></html>
下面會講下DataURL
的優缺點。
7.2DataURL的優缺點
這裏就不做鋪墊直接說
優點:
- 當我們訪問外部資源很麻煩的時候比如跨域限制的時候,可以直接使用
DataURL
,因爲他不需要向外界發起訪問。 - 如果看過我Chrome調試工具NetWork模塊這篇博客的朋友可能會注意到,瀏覽器都有默認的同一時間最多同時加載的資源數量限制,比如Chrome,最多允許同時加載6個資源,其它起源就得排隊等待,但是對於
DataURL
來說,它並不需要向服務器發送Http請求,它就自然不會佔用一個http會話資源。 - 當圖片是在服務器端用程序動態生成,每個訪問用戶顯示的都不同時(emmm,這個我也不是很理解,目前暫時沒碰到過這種場景)
缺點:
- 有心的同學如果去嘗試就會知道,
base64
的數據體積實際上會比原數據大,也就是Data URL形式的圖片會比二進制格式的圖片體積大1/3。所以如果圖片較小,使用DataURL
形式的話是比較有利的,圖片較小即使比原圖片大一些也不會大很多,相比發起一個Http會話這點開銷算什麼。但是如果圖片較大的情況下,使用DataURL
的開銷就會相應增大了,具體如何取捨還得各位同學結合實際場景考量。 - Data URL形式的圖片不會被瀏覽器緩存,這意味着每次訪問這樣頁面時都被下載一次。這是一個使用效率方面的問題——尤其當這個圖片被整個網站大量使用的時候。(這個缺點可以規避,具體可以自行百度或看其它的博客,這裏不細說)
那麼總結一下,DataURL
帶來的便利原因就是一個:它不需要發起http請求;而它的缺點歸納起來就是兩個:體積比原有還要大、不會被緩存。
7.3 本文爲何使用DataURL
看完上面的科普,同學們是不是都瞭解了DataURL
的優缺點,那麼不知道各位是否會好奇,本次前端壓縮爲何使用的是DataURL
呢?
答案其實在第一小節中有,因爲我們想做圖片預覽,而圖片預覽是通過FileReader
對象實現的,FileReader
會去讀文件,並且返回文件的內容,但是返回的內容要麼就是二進制字符串,要麼就是二進制字節數組,這些都不能直接用於圖片展示,只有調用FileReader.readAsDataURL()
返回的DataURL
格式的數據可以直接使用。
8. 代碼
因爲公司使用的前端框架比較非主流,是layUI框架的,所以我的代碼也是基於layUI的前端壓縮(所以你們直接copy我的代碼大概率是跑不起來的),不過核心思想核心方法核心原理在博客中都有體現,只要理解了思想,其實不難將它復刻到你們自己的項目中去。
代碼如下:
//這是layUI的文件上傳組件,不瞭解的朋友也沒必要去看文檔,理解思想就行了
var upload1 = upload.render({
elem: '.uploadImg',
url: releaseUrl,
accept: 'images',
auto: false,
//選擇文件後的鉤子函數
choose: function (obj) {
var that = this;
//預讀本地文件示例,不支持ie8
obj.preview(function (index, file, result) {
console.log("選中的文件:", file);
var index = layer.load(1, {
content: '圖片上傳中....',
shade: 0.2,
success: function (layer) {
layer.find('.layui-layer-content').css({
'paddingTop': '40px',
'width': '60px',
'textAlign': 'center',
'backgroundPositionX': 'center'
});
}
});
//這段開始纔是壓縮的重點,所以關注這段代碼即可
//若圖片超過1M則啓動壓縮
if (file.size > (1024 * 1024)) {
// console.log("大於1m");
canvasDataURL(file, that, function (blob) {
// console.log("壓縮後的二進制Blob對象:",blob);
console.log("壓縮前:" + (file.size / 1024 / 1024) + "M");
console.log("壓縮後:" + (blob.size / 1024 / 1024) + "M");
var compresFile = new File([blob], file.name, {
type: file.type
})
obj.upload(1, compresFile);
})
}
//若圖片不超過1M則無需壓縮,直接傳
else {
var picDom = $(that.item).find("img");
picDom.attr('src', result);
obj.upload(1, file);
}
});
},
})
//下面是壓縮部分的核心代碼
/**
* 通過canvas畫布實現壓縮,並轉化爲base64格式的圖片
* @param {File} file : 圖片
* @param {Object} item :通過item找到當前對象的img標籤
* @param {Function} callback :回調函數
*/
function canvasDataURL(file,item,callback) { //壓縮轉化爲base64
var reader = new FileReader(); //讀取文件的對象
reader.readAsDataURL(file); //對文件讀取,讀取完成後會將內容以base64的形式賦值給result屬性
reader.onload = function (e) { //讀取完成的鉤子
const img = new Image();
const quality = 0.2; // 圖像質量
//先創建canvas畫布,再獲取canvas畫布上的2d繪圖環境,通過這個2d繪圖環境纔可使用繪製API
const canvas = document.createElement('canvas'); //創建canvas畫布
const drawer = canvas.getContext('2d'); //返回一個在畫布上繪製2d圖的環境對象,該對象上包含有canvas繪製2d圖形的API
img.src = this.result;
// console.log("FileReader對象:",this);
//圖片預覽
var picDom = $(item.item).find("img");
picDom.attr('src', this.result); //圖片鏈接(base64)
//圖片壓縮代碼,需要注意的是,img圖片渲染是異步的,所以必須在img的onlaod鉤子中再進行相應操作
img.onload = function () {
canvas.width = img.width;
canvas.height = img.height;
drawer.drawImage(img, 0, 0, canvas.width, canvas.height);
convertBase64UrlToBlob(canvas.toDataURL(file.type, quality), callback);
}
}
}
//下面是上傳部分的核心代碼
/**
* 將base64格式轉化爲Blob格式
* @param {string} urlData : urlData格式的數據,通過這個轉化爲Blob對象
* @param {Function} callback : 回調函數
*/
function convertBase64UrlToBlob(urlData, callback) { //將base64轉化爲文件格式
// console.log("壓縮成base64的對象:",urlData);
const arr = urlData.split(',')
// console.log("arr",arr);
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1]) //atob方法用於解碼base64
// console.log("將base64進行解碼:",bstr);
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
// console.log("Uint8Array:",u8arr);
callback(new Blob([u8arr], {
type: mime
}));
}
9. 總結
關於前端圖片壓縮的壓縮篇告一段落,下一篇博客將介紹前端圖片壓縮的上傳篇,重點是
- Blob對象
- Base64轉Blob的方法
- Uint8Array
- 爲什麼要先壓縮爲Base64(DataURL)?再將Base64(DataURL)轉化爲Blob對象?)。
也歡迎大佬在評論區留言指正博客錯誤的地方,也歡迎像我一樣的小白前端一起在評論區討論。
作者:劉偉C
鏈接:https://juejin.cn/post/6844903495703904263
來源:稀土掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。