- A+
最近有个需求,就是上传图片的时候,图片过大,需要压缩一下图片再上传。
需求虽然很容易理解,但要做到,不是那么容易的。
这里涉及到的知识有点多,不多说,本篇博客有点重要呀!
一、图片URL转Blob(图片大小不变)
注意点:图片不能跨域!!!
方式一:通过XHR请求获取
function urlToBlobByXHR(url) { const xhr = new XMLHttpRequest(); xhr.open("get", url); xhr.responseType = "blob"; // 设置响应请求格式 xhr.onload = (e) => { if (e.target.status == 200) { console.log(e.target.response); // e.target.response返回的就是Blob。 return e.target.response;// 这样是不行的 } else { console.log("异常"); } }; xhr.send(); } urlToBlobByXHR("图片URL"); // 调用
我们知道,XHR操作是异步的,只有在onload方法里面才能获取到Blob,相应的业务代码也要写到里面。怎么能够做到调用这个方法,直接得到Blob结果呢?
Promise便解决了诸如此类的痛点。
function urlToBlobByXHR(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("get", url); xhr.responseType = "blob"; xhr.onload = (e) => { if (e.target.status == 200) { resolve(e.target.response); // resolve } else { reject("异常"); // reject } }; xhr.send(); }) } async f() { try { console.log(await urlToBlobByXHR(this.imgUrl)); // 直接返回Blob } catch (e) { console.log(e); } } f(); // 调用
方式二:通过canvas转化(图片大小会变大很多)
基本原理:就是新建一个canvas元素,然后在里面将图片画上去,接着利用canvas转为Blob。
function canvasToBlob(imgUrl) { return new Promise((resolve, reject) => { const imgObj = new Image(); imgObj.src = imgUrl; imgObj.onload = () => { const canvasObj = document.createElement("canvas"); const ctx = canvasObj.getContext("2d"); canvasObj.width = imgObj.naturalWidth; canvasObj.height = imgObj.naturalHeight; ctx.drawImage(imgObj, 0, 0, canvasObj.width, canvasObj.height); canvasObj.toBlob((blob) => { resolve(blob); }); } }) } const blobCanvas = await canvasToBlob(imgUrl); // 调用,直接获取到blob
不过呢,利用canvas转化,图片会变大很多,在canvas上面画图片,期间图片分辨率会改变,加上可能还有图片解析的原因,会导致图片变大很多。
而且canvas是可以截图的,不过这一点是人为可以控制的。
二、图片压缩
原理:我们知道在canvas里面画图,canvas相当于图片的容器,既然是容器,那便可以控制容器的宽高,相应的改变图片的宽高,通过这一点,不就可以缩小图片了吗?
不过要注意的是,缩小图片要等比例的缩小,虽然提供的接口里面支持更改图片清晰度,但个人并不建议这么做,至于原因自己想吧。
版本一:
// imageUrl:图片URL,图片不能跨域 // maxSize:图片最大多少M // scale:图片放大比例 function compressImg1(imageUrl, maxSize = 1, scale = 0.8, imgWidth, imgHeight) { let maxSizeTemp = maxSize * 1024 * 1024; return new Promise((resolve, reject) => { const imageObj = new Image(); imageObj.src = imageUrl; imageObj.onload = () => { const canvasObj = document.createElement("canvas"); const ctx = canvasObj.getContext("2d"); if (imgWidth && imgHeight) { // 等比例缩小 canvasObj.width = scale * imgWidth; canvasObj.height = scale * imgHeight; } else { canvasObj.width = imageObj.naturalWidth; canvasObj.height = imageObj.naturalHeight; } ctx.drawImage(imageObj, 0, 0, canvasObj.width, canvasObj.height); canvasObj.toBlob((blob) => { resolve({ blob, canvasObj }); }); } }).then(({ blob, canvasObj }) => { if (blob.size / maxSizeTemp < maxSize) { let file = new File([blob], `test${imageUrl.substring(imageUrl.lastIndexOf("."))}`); return Promise.resolve({ blob, file }); } else { return compressImg1(imageUrl, maxSize, scale, canvasObj.width, canvasObj.height); // 递归调用 } }) } const { blob } = await compressImg1("图片地址"); // 调用
需求是实现了,但用到了递归,性能完全由缩小比例跟图片大小决定。
图片过大的话或者缩小比例大了点,会导致不断递归,性能低下,这是肯定的。
以上还有两个耗时的操作:
1、不断请求图片
2、不断操作DOM
版本二:
有个潜规则,能不用递归就不用递归。
试想,怎样一步到位可以把图片缩小到需要的大小呢?再深入直接一点,如何得到有效的scale,等比例缩小后就能使图片缩小到想要的程度呢?
然后再把以上两个耗时操作再优化一下,只需加载一次图片。便得到了版本二。
function compressImg2(imageUrl, maxSize = 1, scale = 1) { let maxSizeTemp = maxSize * 1024 * 1024; return new Promise((resolve, reject) => { const imageObj = new Image(); // 只需加载一次图片 imageObj.src = imageUrl; imageObj.onload = () => { const canvasObj = document.createElement("canvas"); // 只需创建一次画布 const ctx = canvasObj.getContext("2d"); canvasObj.width = imageObj.naturalWidth; canvasObj.height = imageObj.naturalHeight; ctx.drawImage(imageObj, 0, 0, canvasObj.width, canvasObj.height); canvasObj.toBlob((blob1) => { resolve({ imageObj, blob1, canvasObj, ctx }); }); } }).then(({ imageObj, blob1, canvasObj, ctx }) => { if (blob1.size / maxSizeTemp < maxSize) { let file = new File([blob1], `test${imageUrl.substring(imageUrl.lastIndexOf("."))}`); return Promise.resolve({ blob: blob1, file }); } else { const ratio = Math.round(blob1.size / maxSizeTemp); // 比例 canvasObj.width = (imageObj.naturalWidth / ratio) * scale; // 比例调整 canvasObj.height = (imageObj.naturalHeight / ratio) * scale; ctx.drawImage(imageObj, 0, 0, canvasObj.width, canvasObj.height); return new Promise((resolve) => { canvasObj.toBlob((blob2) => { let file = new File([blob2], `test${imageUrl.substring(imageUrl.lastIndexOf("."))}`); resolve({ blob: blob2, file }); }); }) } }) }
版本三(Promise转为async await)
我们知道Promise跟asnc await是等价的。
async function compressImg(imageUrl, maxSize = 1, scale = 1) { let maxSizeTemp = maxSize * 1024 * 1024; const { imageObj, blob1, canvasObj, ctx } = await new Promise((resolve, reject) => { const imageObj = new Image(); imageObj.src = imageUrl; imageObj.onload = () => { const canvasObj = document.createElement("canvas"); const ctx = canvasObj.getContext("2d"); canvasObj.width = imageObj.naturalWidth; canvasObj.height = imageObj.naturalHeight; // console.log(canvasObj); ctx.drawImage(imageObj, 0, 0, canvasObj.width, canvasObj.height); canvasObj.toBlob((blob1) => { // console.log('blob1', blob1); resolve({ imageObj, blob1, canvasObj, ctx }); }); }; }); if (blob1.size / maxSizeTemp < maxSize) { let file = new File([blob1], `test${imageUrl.substring(imageUrl.lastIndexOf("."))}`); return Promise.resolve({ blob: blob1, file }); } else { // const ratio = Math.round(Math.sqrt(blob1.size / maxSizeTemp)); const ratio = Math.round(blob1.size / maxSizeTemp); // console.log('ratio', ratio); canvasObj.width = (imageObj.naturalWidth / ratio) * scale; canvasObj.height = (imageObj.naturalHeight / ratio) * scale; // console.log(canvasObj); ctx.drawImage(imageObj, 0, 0, canvasObj.width, canvasObj.height); const { blob: blob2, file } = await new Promise((resolve) => { canvasObj.toBlob((blob2) => { // console.log('blob2', blob2); let file = new File([blob2], `test${imageUrl.substring(imageUrl.lastIndexOf("."))}`); resolve({ blob: blob2, file }); }); }) return { blob: blob2, file }; } }
三、详细讲解下Promise
简单的一个例子
let p = new Promise((resolve) => { setTimeout(() => { resolve(123456); // 5秒后输出123456 }, 5000); }); p.then((s) => { console.log(s); // 通过then的参数就可以获取到结果 }); let s = await p; // async await转换,简化then写法 console.log(s);
其实呢,Promise本质上就是回调函数的使用,而Promise主要是为了解决回调地狱(回调函数嵌套)而出现的,async await写法主要是为了简化方便。
咱来模拟一下最简单的Promise,手写一个简单一点的。
// 首先定义一下Promise状态 const status = { pending: "pending", fulfilled: "fulfilled", rejected: "rejected", };
不支持异步(先来个简单的)
function MyPromise(executor) { const self = this;// this指向 self.promiseStatus = status.pending; self.promiseValue = undefined; self.reason = undefined; function resolve(value) { if (self.promiseStatus == status.pending) { self.promiseStatus = status.fulfilled; self.promiseValue = value; } } function reject(reason) { if (self.promiseStatus == status.pending) { self.promiseStatus = status.rejected; self.reason = reason; } } try { executor(resolve, reject); // 在这里比较难以理解,函数resolve作为函数executor的参数,new MyPromise调用的时候,传的也是个函数。 } catch (e) { reject(e); } } MyPromise.prototype.then = function (onResolve, onReject) { // 利用原型添加方法 const self = this; if (self.promiseStatus == status.fulfilled) { onResolve(self.promiseValue); } if (self.promiseStatus == status.rejected) { onReject(self.reason); } }; // 调用 const myPromise = new MyPromise((resolve, reject) => { // MyPromise的参数也是个函数 resolve(123456); // 暂时不支持异步 }); myPromise.then((data) => { console.log("data", data); // 输出123456 });
支持异步的
function MyPromise(executor) { const self = this; self.promiseStatus = status.pending; self.promiseValue = undefined; self.reason = undefined; self.onResolve = []; self.onReject = []; function resolve(value) { if (self.promiseStatus == status.pending) { self.promiseStatus = status.fulfilled; self.promiseValue = value; self.onResolve.forEach((fn) => fn(value)); //支持异步 } } function reject(reason) { if (self.promiseStatus == status.pending) { self.promiseStatus = status.rejected; self.reason = reason; self.onReject.forEach((fn) => fn(reason)); // //支持异步 } } try { executor(resolve, reject); } catch (e) { reject(e); } } MyPromise.prototype.then = function (onResolve, onReject) { const self = this; if (self.promiseStatus == status.fulfilled) { onResolve(self.promiseValue); } if (self.promiseStatus == status.rejected) { onReject(self.reason); } if (self.promiseStatus == status.pending) { self.onResolve.push(onResolve); self.onReject.push(onReject); } }; // 调用 const myPromise = new MyPromise((resolve, reject) => { setTimeout(() => { resolve(123456); // 异步 }, 3000); }); myPromise.then((data) => { console.log("data", data); // 输出123456 });
个人觉得,能明白大致原理,会用就行了,至于能不能手写一个Promise并不是很重要的,不断重复造轮子没啥意思,
但是呢,理解其大概思路以及实现所用到的思想还是很重要的,对成长的帮助很大。
总结
图片压缩还有待优化,
Promise,大家应该都很熟悉,用的非常多,可真正会用的人并不是太多的。
最后,祝大家中秋快乐!