我們都知道在“寸土寸金”的互聯網時代, 速度是第一競爭力, 雖然我們的5G發展已經搖搖領先, 但是也經不住用戶在一個網頁里傳很多“巨無霸”圖片, 最終導致的結果就是頁面“龜速”打開......
那么作為技術人, 當然也有一堆的解決方案, 比如:
當然聰明的小伙伴也會將上面的方案組合, 設計更優秀的圖片“提速”方案.
今天不會和大家把所有方案都介紹一遍, 因為網上也有很多實踐, 接下來會從前端技術提升的角度, 分享一下如何用原生 javascript, 實現從圖片上傳到圖片自定義壓縮的完整方案. 大家可以把文章中介紹的方案直接用于自己的實際開發中, 或者基于它設計更棒的圖片壓縮方案.
前端實現圖片壓縮無非就是在用戶上傳圖片文件后, 將file轉換成image對象, 然后再利用canvas 及其 api 將圖片壓縮成指定體積. 如下流程:
首先我們先實現將file轉換成image對象, 這里我們用到了FileReader API, 代碼如下:
// 壓縮前將file轉換成img對象
function readImg(file:File) {
return new Promise((resolve, reject) => {
const img = new Image()
const reader = new FileReader()
reader.onload = function(e:any) {
img.src = e.target.result
}
reader.onerror = function(e) {
reject(e)
}
reader.readAsDataURL(file)
img.onload = function() {
resolve(img)
}
img.onerror = function(e) {
reject(e)
}
})
}
這里使用 promise 來設計生成圖片數據的方法, 接下來我們看看核心的圖片壓縮源碼:
/**
* 壓縮圖片
* @param img 被壓縮的img對象
* @param type 壓縮后轉換的文件類型
* @param mx 觸發壓縮的圖片最大寬度限制
* @param mh 觸發壓縮的圖片最大高度限制
* @param quality 圖片質量
*/
function compressImg(img: any, type:string, mx: number, mh: number, quality:number = 1) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const { width: originWidth, height: originHeight } = img
// 最大尺寸限制
const maxWidth = mx
const maxHeight = mh
// 目標尺寸
let targetWidth = originWidth
let targetHeight = originHeight
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > 1) {
// 寬圖片
targetWidth = maxWidth
targetHeight = Math.round(maxWidth * (originHeight / originWidth))
} else {
// 高圖片
targetHeight = maxHeight
targetWidth = Math.round(maxHeight * (originWidth / originHeight))
}
}
canvas.width = targetWidth
canvas.height = targetHeight
context?.clearRect(0, 0, targetWidth, targetHeight)
// 圖片繪制
context?.drawImage(img, 0, 0, targetWidth, targetHeight)
canvas.toBlob(function(blob) {
resolve(blob)
}, type || 'image/png', quality)
})
}
這里通過控制 canvas的寬高, 以及對 canvas 的 toBlob設置參數, 來實現自定義的圖片壓縮.
如果大家對代碼有不理解的地方, 也可以在文末發表問題, 我會做出對應的解答.
實現 HTML 壓縮,可以使用 JavaScript 中的正則表達式來去除 HTML 中的空格和注釋。以下是一個簡單的 HTML 壓縮函數:
function compressHTML(html) {
// 去除注釋
html = html.replace(/<!--[\s\S]*?-->/g, "");
// 去除多余空白
html = html.replace(/\s+/g, " ");
// 去除標簽之間空格
html = html.replace(/>\s+</g, "><");
return html.trim();
}
該函數首先使用正則表達式去除 HTML 中的注釋。然后,它使用另一個正則表達式去除 HTML 中的多余空格。最后,它使用另一個正則表達式去除標簽之間的空格。
為了測試該函數,您可以創建一個 HTML 文件,并在其中添加一些冗余的空格和注釋。例如:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<!-- This is a comment -->
<h1> Welcome to my website! </h1>
<p> This is some text. </p>
</body>
</html>
然后,您可以在Node.JS中使用以下代碼將 HTML 文件加載為字符串并壓縮它:
// 加載 HTML 文件
const fs = require("fs");
const html = fs.readFileSync("index.html", "utf8");
// 壓縮 HTML
const compressedHtml = compressHTML(html);
console.log(compressedHtml);
輸出是一個壓縮后的 HTML 字符串,其中不包含注釋或冗余空格。
或者直接在IE中測試,代碼如下:
function compressHTML(html) {
// 去除注釋
html = html.replace(/<!--[\s\S]*?-->/g, "");
// 去除多余空白
html = html.replace(/\s+/g, " ");
// 去除標簽之間空格
html = html.replace(/>\s+</g, "><");
return html.trim();
}
var html =`
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<!-- This is a comment -->
<h1> Welcome to my website! </h1>
<p> This is some text. </p>
</body>
</html>
`;
console.log(compressHTML(html));
運行效果:
景
實際生產中經常遇到這樣的場景:為減小服務器壓力,上傳附件尤其是圖片的時候,往往需要限制上傳文件的大小。而限制的方案也有兩種,一種就是限制用戶可上傳的文件大小,由用戶來選擇上傳的文件和如果文件過大由用戶自行進行壓縮裁剪;另一種就是由服務進行圖片的壓縮和大小控制然后再上傳到服務器。這里主要介紹的是第二種方案。
回到頂部
主要技術
前邊有介紹過證書的生成和下載,其中就有證書的壓縮和打包的相關操作,感興趣的可以看下本人的那篇文章。這里同樣是采用的該原理,步驟如下:
回到頂部
關鍵步驟
圖片文件-->文件流(base64位編碼)-->canvas-->壓縮-->生成壓縮后的文件-->上傳。
這里的壓縮過程,做了相應的優化。優化方案有兩種,一種是重復壓縮,一種是計算比例壓縮。
而由于壓縮比和文件大小并不是正比例關系,所有可以保險起見再乘以一個系數。比如:quality: 1024*0.7/fileObj.size(0.7是保險系數,1024是限制大小1M的意思,可根據個人需要自行調整參數,也可以封裝成接口參數統一修改)
這里還自行封裝了一個進度組件,使用的是原生js。
回到頂部
代碼
代碼和相關注釋如下:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>文件壓縮上傳</title> <script type="text/javascript"> /* 三個參數 file:一個是文件(類型是圖片格式), w:一個是文件壓縮的后寬度,寬度越小,字節越小 objDivOrCallback:一個是容器或者回調函數 photoCompress() */ function photoCompress(file,w,objDivOrCallback) { var ready = new FileReader() /*開始讀取指定的Blob對象或File對象中的內容. 當讀取操作完成時,readyState屬性的值會成為DONE,如果設置了onloadend事件處理程序,則調用之.同時,result屬性中將包含一個data: URL格式的字符串以表示所讀取文件的內容.*/ ready.readAsDataURL(file) ready.onload = function() { var re = this.result canvasDataURL(re, w, objDivOrCallback) } } function canvasDataURL(path, obj, callback) { var img = new Image() img.src = path img.onload = function(){ var that = this // 默認按比例壓縮 var w = that.width, h = that.height, scale = w / h w = obj.width || w h = obj.height || (w / scale) var quality = 0.7 // 默認圖片質量為0.7 //生成canvas var canvas = document.createElement('canvas') var ctx = canvas.getContext('2d') // 創建屬性節點 var anw = document.createAttribute("width") anw.nodeValue = w var anh = document.createAttribute("height") anh.nodeValue = h canvas.setAttributeNode(anw) canvas.setAttributeNode(anh) ctx.drawImage(that, 0, 0, w, h) // 圖像質量 if(obj.quality && obj.quality <= 1 && obj.quality > 0) { quality = obj.quality } // quality值越小,所繪制出的圖像越模糊 var base64 = canvas.toDataURL('image/jpeg', quality) // 這里不能直接quality: 0.2,因為這樣就相當于還是在原來的大小的基礎上壓縮 var bl = convertBase64UrlToBlob(base64) // 如果還大于1M,繼續壓縮--代碼待優化,可以減去重復生成文件和轉碼的過程 if (bl.size/1024 > 1025) { // 其實也可以在這里直接寫一個匹配壓縮比直到大小小于1的方法 photoCompress(bl, { quality: 0.5 * obj.quality }, callback) } else { callback(bl) } // 回調函數返回base64的值--改為返回文件對象 // callback(base64) } } /** * 將以base64的圖片url數據轉換為Blob * @param urlData * 用url方式表示的base64圖片數據 */ function convertBase64UrlToBlob(urlData) { var arr = urlData.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n) while(n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], {type:mime}) } var xhr //上傳文件方法 function UpladFile() { var fileObj = document.getElementById("file").files[0] // js 獲取文件對象 var url = "http://api.test.cn/file/publicFile/upload" // 接收上傳文件的后臺地址 var form = new FormData() // FormData 對象 if(fileObj.size/1024 > 1025) { //大于1M,進行壓縮上傳 photoCompress(fileObj, { // 這里還有一種方案,那就是這里的quality改為計算壓縮比(由于壓縮比和文件大小并不是正比例關系,所有可以保險起見再乘以一個系數) // 壓縮比計算的方案:quality: 1024*0.7/fileObj.size--0.7是保險系數--這些參數可以進一步封裝 quality: 0.2 // }, function(base64Codes){ // 修改為返回文件對象 }, function(bl){ //console.log("壓縮后:" + base.length / 1024 + " " + base); // var bl = convertBase64UrlToBlob(base64Codes) // form.append("file", bl, "file_"+Date.parse(new Date())+".jpg"); // 文件對象 form.append("multipartFile", bl, "file_"+Date.parse(new Date())+".jpg") // 文件對象 xhr = new XMLHttpRequest() // XMLHttpRequest 對象 xhr.open("post", url, true) //post方式,url為服務器請求地址,true 該參數規定請求是否異步處理。 xhr.setRequestHeader("enctype", "multipart/form-data") // 設置請求頭 xhr.setRequestHeader("Authorization", "Bearer 8d782bb1-768f-4fa7-80d2-5e2b6d6a6f64") // 設置請求頭 // open后才可以設置頭 xhr.onload = uploadComplete //請求完成 xhr.onerror = uploadFailed //請求失敗 xhr.upload.onprogress = progressFunction//【上傳進度調用方法實現】 xhr.upload.onloadstart = function(){//上傳開始執行方法 ot = new Date().getTime() //設置上傳開始時間 oloaded = 0//設置上傳開始時,以上傳的文件大小為0 }; xhr.send(form) //開始上傳,發送form數據 }) } else { //小于等于1M 原圖上傳 // form.append("file", fileObj) // 文件對象 form.append("multipartFile", fileObj) // 文件對象 xhr = new XMLHttpRequest() // XMLHttpRequest 對象 xhr.open("post", url, true) //post方式,url為服務器請求地址,true 該參數規定請求是否異步處理。 xhr.setRequestHeader("enctype", "multipart/form-data") // 設置請求頭 xhr.setRequestHeader("Authorization", "Bearer 8d782bb1-768f-4fa7-80d2-5e2b6d6a6f64") // 設置請求頭 // open后才可以設置頭 xhr.onload = uploadComplete //請求完成 xhr.onerror = uploadFailed //請求失敗 xhr.upload.onprogress = progressFunction//【上傳進度調用方法實現】 xhr.upload.onloadstart = function() {//上傳開始執行方法 ot = new Date().getTime() //設置上傳開始時間 oloaded = 0//設置上傳開始時,以上傳的文件大小為0 } xhr.send(form) //開始上傳,發送form數據 } } //上傳成功響應 function uploadComplete(evt) { //服務斷接收完文件返回的結果 var data = JSON.parse(evt.target.responseText) if(data.code === 200) { uploadSuccess() } else { uploadFailed() } } //上傳失敗 function uploadFailed(evt) { alert("上傳失敗!") } //上傳成功 function uploadSuccess(evt) { alert("上傳成功!") } //取消上傳 function cancleUploadFile(){ xhr.abort() } //上傳進度實現方法,上傳過程中會頻繁調用該方法 function progressFunction(evt) { var progressBar = document.getElementById("progressBar") var percentageDiv = document.getElementById("percentage") // event.total是需要傳輸的總字節,event.loaded是已經傳輸的字節。如果event.lengthComputable不為真,則event.total等于0 if (evt.lengthComputable) {// progressBar.max = evt.total progressBar.value = evt.loaded percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%" } var time = document.getElementById("time") var nt = new Date().getTime()//獲取當前時間 var pertime = (nt-ot)/1000 //計算出上次調用該方法時到現在的時間差,單位為s ot = new Date().getTime() //重新賦值時間,用于下次計算 var perload = evt.loaded - oloaded //計算該分段上傳的文件大小,單位b oloaded = evt.loaded//重新賦值已上傳文件大小,用以下次計算 //上傳速度計算 var speed = perload/pertime//單位b/s var bspeed = speed var units = 'b/s'//單位名稱 if(speed/1024>1) { speed = speed/1024 units = 'k/s' } if(speed/1024>1) { speed = speed/1024 units = 'M/s' } speed = speed.toFixed(1) //剩余時間 var resttime = ((evt.total-evt.loaded)/bspeed).toFixed(1) time.innerHTML = ',速度:'+speed+units+',剩余時間:'+resttime+'s' if(bspeed==0) time.innerHTML = '上傳已取消' } </script> </head> <body> <progress id="progressBar" value="0" max="100" style="width: 300px;"></progress> <span id="percentage"></span><span id="time"></span> <br /><br /> <input type="file" id="file" name="myfile" accept="image/x-png, image/jpg, image/jpeg, image/gif"/> <input type="button" onclick="UpladFile()" value="上傳" /> <input type="button" onclick="cancleUploadFile()" value="取消" /> </body> </html>
此處是借鑒網上思路的基礎上的個人修改完善后的代碼, 并且有待有時間的時候做進一步封裝優化和封裝成npm組件以及vue組件。
擴展
png圖片的另一種壓縮方案
png的簡介
什么是png:
PNG的全稱叫便攜式網絡圖型(Portable Network Graphics)是目前最流行的網絡傳輸和展示的圖片格式,原因有如下幾點:
當初就是因為png的透明特性才開始喜歡它的。
png的類型:
png圖片的數據編碼:
PNG圖片的數據結構其實跟http請求的結構很像,都是一個數據頭,后面跟著很多的數據塊,如下圖所示:
使用16進制編碼打開png圖片,部分編碼示例如下:
8950 4e47 0d0a 1a0a:這個是PNG圖片的頭,所有的PNG圖片的頭都是這一串編碼,圖片軟件通過這串編碼判定這個文件是不是PNG格式的圖片。
0000 000d:是iHDR數據塊的長度,為13。
4948 4452:是數據塊的type,為IHDR,之后緊跟著是data。
0000 0292:是圖片的寬度。
0000 024e:是高度。
以此類推,每一段十六進制編碼就代表著一個特定的含義。感興趣的可以自行百度。
所以,顏色重復度越大的、越接近的(漸變的顏色或透明度等),編碼重復度就越大,就越容易壓縮。
壓縮原理:
png圖片用差分編碼(Delta encoding)對圖片進行預處理,處理每一個的像素點中每條通道的值。
壓縮階段會將預處理階段得到的結果進行Deflate壓縮,它由 Huffman 編碼 和 LZ77壓縮構成。
壓縮后的結果就是一串處理后的編碼,保存到數據庫中,占用空間會小很多,在使用的時候,再進行逆向解析渲染。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。