我们都知道在“寸土寸金”的互联网时代, 速度是第一竞争力, 虽然我们的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小时内与您取得联系。