• 高级前端进阶(六)


    最近有个需求,就是上传图片的时候,图片过大,需要压缩一下图片再上传。
    需求虽然很容易理解,但要做到,不是那么容易的。
    这里涉及到的知识有点多,不多说,本篇博客有点重要呀!

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

    最后,祝大家中秋快乐!
  • 相关阅读:
    基于android的健身管理APP系统-计算机毕业设计
    fastadmin如何让后台的日期显示成年月日格式
    Gateway新一代网关
    高项_第18-20章组织级项目管理&流程管理&项目集管理
    idea连接redis
    一起Talk Android吧(第三百七十三回:多线程版Timer)
    Kubernetes(K8s):容器化应用的航空母舰
    1000套web前端期末大作业 HTML+CSS+JavaScript网页设计实例 企业网站制作【建议收藏】
    MongoDB聚合运算符:$dateAdd
    在 Linux 上搭建 Java Web 项目环境(最简单的进行搭建)
  • 原文地址:https://www.cnblogs.com/ywjbokeyuan/p/16599152.html