最近有个需求,就是上传图片的时候,图片过大,需要压缩一下图片再上传。
需求虽然很容易理解,但要做到,不是那么容易的。
这里涉及到的知识有点多,不多说,本篇博客有点重要呀!
一、图片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,大家应该都很熟悉,用的非常多,可真正会用的人并不是太多的。
最后,祝大家中秋快乐!