1

高级前端进阶(六) - 一曲风流唯少年

 1 year ago
source link: https://www.cnblogs.com/ywjbokeyuan/p/16599152.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
最近有个需求,就是上传图片的时候,图片过大,需要压缩一下图片再上传。
需求虽然很容易理解,但要做到,不是那么容易的。
这里涉及到的知识有点多,不多说,本篇博客有点重要呀! deyi.gif

一、图片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相当于图片的容器,既然是容器,那便可以控制容器的宽高,相应的改变图片的宽高,通过这一点,不就可以缩小图片了吗?
不过要注意的是,缩小图片要等比例的缩小,虽然提供的接口里面支持更改图片清晰度,但个人并不建议这么做,至于原因自己想吧。huaixiao.gif

// 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,大家应该都很熟悉,用的非常多,可真正会用的人并不是太多的。cahan.gif

1600391-20220907204432191-1451840557.jpg
最后,祝大家中秋快乐!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK