阅读 1229

前端图片压缩上传(压缩篇):可能是最适合小白的前端图片压缩文章了!

为什么说这是一篇比较适合小白的前端图片压缩文章呢?因为我也是一个刚工作半年的前端小白,最近接到了一个前端图片压缩上传的任务,通过各种百度博客完成了这项任务,但是任务完成后对各种技术细节却还不是特别理解,所以我针对我不理解的每一个技术细节都进行了记录和学习,最后形成本篇博客,我觉得我不了解的地方可能也会有别的同学不是很了解,所以本篇博客科普向比较重,技术深度可能达不到大佬们的眼界,但也是自己的记录和学习,加油!!!

1. 前言

如果您只是为了copy代码实现功能,建议您不要看这篇博客了。如果您是copy了代码实现了功能,想回来了解具体的实现流程、实现原理以及部分科普,我觉得本篇博客能给你带来不小的收获。

2. 任务背景

最近的项目有一个技术场景,简单来说就是用户需要上传图片至服务器。就是这么一个简单的技术场景,但是用户是不可控的,他们可能上传的图片会是几M甚至十几M的大小,如此大的图片必然会导致上传时间过长,用户感知不好,服务器压力大等不良影响。

作为组里唯一一个前端,老大就给了我一个任务,在用户上传图片前把图片压缩了再上传。那么本篇博客主要讲解的就是图片压缩中我是如何进行压缩的,之后的博客会讲解如何将压缩的图片进行上传。

3. 流程说明

本文实现的功能流程如下:

  1. 用户通过input框选择图片
  2. 使用FileReader进行图片预览
  3. 将图片绘制到canvas画布上
  4. 使用canvas画布的能力进行图片压缩
  5. 将压缩后的Base64(DataURL)格式的数据转换成Blob对象进行上传

简要流程图如下:

各位同学们看到这里,不知道会不会有跟我当时一样的疑惑?什么是FileReader?我知道canvas是画布,但是具体是怎么样用它进行图片压缩的呢?Base64我知道但是什么是DataURL格式的数据呢?Blob对象又是什么?

同学们,不着急,对于这些我都会进行一一讲解,因为这也是作为一个小白的我初次见到这些陌生名词时的疑惑。

4. 效果预览

5. FileReader

FileReader这个对象是做什么的?在本次图片压缩中起到了什么作用?

5.1 FileReader是什么

我们先来看看MDN上对它的解释:FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用FileBlob对象指定要读取的文件或数据。

通俗来讲,就是这个对象是用来读取File对象或Blob对象的。File对象就是<input type="file">获取到的对象,而Blob(二进制)对象在本文的第5点有讲解。

5.2 FileReader的钩子与方法

作为一个js原生的用于读取文件的对象,FileReader本身就有较为完整的钩子函数以及一些实例方法,但是本文主要介绍图片压缩,所以在这里只重点讲本文使用到的1个钩子函数和1个实例方法,对其它的钩子和方法都不做详细介绍。

FileReader.onload:处理load事件。即该钩子在读取操作完成时触发,通过该钩子函数可以完成例如读取完图片后进行预览的操作,或读取完图片后对图片内容进行二次处理等操作。

FileReader.readAsDataURL:读取方法,并且读取完成后,result属性将返回 Data URL 格式(Base64 编码)的字符串,代表图片内容。

除了用到的这个钩子和这个实例方法外,FileReader对象还有onabortonerroronloadstartonloadendonprogress等钩子;也有abort()readAsArrayBufferreadAsBinaryString等实例方法,在次就不过多描述。

5.3 FileReader在图片压缩中的作用

onload这个钩子对上传的图片实现了预览,并且进行了图片压缩处理。通过readAsDataURL()方法进行了文件的读取,并且通过result属性拿到了图片的Base64(DataURL)格式的数据,然后通过该数据实现了图片预览的功能。有的同学看到这里是不是有点好奇,为什么拿到了这个Base64(DataURL)格式的数据就能直接展示处图片了呢?不要紧,往下看,我会在后文中解释这个DataURL格式的神奇。

FileReader部分代码如下:

function canvasDataURL(file,item,callback) { //压缩转化为base64
            var reader = new FileReader();      //读取文件的对象
            reader.readAsDataURL(file);         //对文件读取,读取完成后会将内容以base64的形式赋值给result属性
            reader.onload = function (e) {      //读取完成的钩子
                console.log("原始二进制字符串:",this.result.toString());
                const img = new Image();
                const quality = 0.2; // 图像质量
                const canvas = document.createElement('canvas');
                const drawer = canvas.getContext('2d');
                img.src = this.result;
                console.log("FileReader对象:",this);
                //图片预览
                var picDom = $(item.item).find("img");  
                picDom.attr('src', this.result); //图片链接(base64)
                //图片压缩转码
                img.onload = function () {
                    canvas.width = img.width;
                    canvas.height = img.height;
                    drawer.drawImage(img, 0, 0, canvas.width, canvas.height);
                    convertBase64UrlToBlob(canvas.toDataURL(file.type, quality), callback);
                }
            }
        }
复制代码

6. canvas(图片压缩的核心)

canvas元素众所周知是画布,那么canvas在图片压缩中起到了什么作用?实现图片压缩的核心内容主要是使用到了canvas的什么方法?

6.1 Canvas在图片压缩中的作用

实现图片压缩最核心的地方就在canvas这里,我们先使用CanvasRenderingContext2D.drawImage()方法将选中的图片文件在画布上绘制出来,再使用Canvas.toDataURL()将画布上的图片信息转换成base64(DataURL)格式的数据。有同学会问,那么是在哪儿实现的压缩?其实压缩的核心就在Canvas.toDataURL()方法的quality参数上了,下面我们会具体介绍本文中使用到的2个canvas画布上的方法。

6.2 CanvasRenderingContext2D.drawImage()

语法如下:

context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
复制代码

这个方法是CanvasRenderingContext2D上的绘制图片的方法,如果有朋友想了解CanvasRenderingContext2Dcanvas之间的联系和区别的话可以自行了解,这里不过多赘述。

首先介绍一下该方法所接受的9个参数:

image:Object;绘制在Canvas上的元素,可以是各类Canvas图片资源),如图片,SVG图像,Canvas元素本身等。

dx:Number;在Canvas画布上规划一片区域用来放置图片,dx就是这片区域的左上角横坐标。

dy:Nmuber;在Canvas画布上规划一片区域用来放置图片,dy就是这片区域的左上角纵坐标。

dWidth:Number;在Canvas画布上规划一片区域用来放置图片,dWidth就是这片区域的宽度。

dHeight:Number;在在Canvas画布上规划一片区域用来放置图片,dHeight就是这片区域的高度。

sx:Number;表示图片元素绘制在Canvas画布上起始横坐标。

sy:Number;表示图片元素绘制在Canvas画布上起始纵坐标。

sWidth:Number;表示图片元素从坐标点开始算,多大的宽度内容绘制Canvas画布上。

sHeight;表示图片元素从坐标点开始算,多大的高度内容绘制Canvas画布上。

很多同学看到这里这么多个参数是不是有点懵逼了?不用慌,我也很懵逼,其实我对canvas也不是很了解,不过没关系我们直接看看本文中使用到这个方法时我们传递了什么参数

//代码
//先创建canvas画布,再获取canvas画布上的2d绘图环境,通过这个2d绘图环境才可使用绘制API
const canvas = document.createElement('canvas');  
//返回一个在画布上绘制2d图的环境对象,该对象上包含有canvas绘制2d图形的API
const drawer = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
drawer.drawImage(img, 0, 0, canvas.width, canvas.height);


//实际传递的参数
drawer.drawImage(image, dx, dy, dWidth, dHeight)
复制代码

结合上面列举的9个参数,我们可以知道我们实际上只使用到了5个参数。我们传递了1个图片,紧接着我们定义了canvas上的起始区域坐标点以及这个canvas上放置图片的区域的宽高。

实质上,我们设置的canvas放置图片区域的宽高大小跟图片本身是一模一样的,因为在本文中,使用该方法的目的不在于在canvas上展示一张图片给用户看,而是在于在canvas上绘制出这张图片,这样我们才能使用接下来的的这个Canvas.toDataURl()方法

6.3 Canvas.toDataURl()

咚!咚!咚!敲重点了,这个方法才是本文中实现图片压缩的核心。

语法如下:

canvas.toDataURL(mimeType, quality);
复制代码

Canvas.toDataURl()方法可以将canvas画布上的信息转换为base64(DataURL)格式的图像信息,纯字符的图片表示形式。该方法接收2个参数:

mimeType(可选):String;表示需要转换的图像的mimeType类型。默认值是image/png,还可以是image/jpeg,甚至image/webp(前提浏览器支持)等。

quailty(可选):Number;quality表示转换的图片质量。范围是0到1。此参数要想有效,图片的mimeType需要是image/jpeg或者image/webp,其他mimeType值无效。默认压缩质量是0.92。

到这里,很多同学就可以知道了,前端图片压缩的核心方法,是不是就在这个方法的quailty参数上面呢?好了,让我们看看本文使用到该方法的地方:

const canvas = document.createElement('canvas'); 
const quality = 0.2; //设置压缩比例
canvas.toDataURL(file.type, quality)
复制代码

可以看到,本文中设置了压缩质量为0.2,需要注意的是不是说压缩质量设置为0.2实际压缩效果就为5倍压缩(在本文中,压缩质量设置成了0.2但实际压缩效果确实7-9倍),简单的说,当到达了这一步以后,其实图片已经完成了压缩,我们已经可以直接拿着返回的base64(DataURL)格式的数据去渲染图片,但是,如果你的目的是将图片先进行压缩,压缩后再上传给服务器,并且服务器只接受二进制的图片信息的话,那就得好好考虑怎么将base64转换成二进制Blob对象了,关于Blob,不要着急,我会在下一篇上传篇中对它进行科普。

小tips:该方法为同步方法,如果需要转换的Canvas尺寸很大,则会阻塞脚本的运行,因此需要注意控制Canvas的尺寸。

6.4 Canvas.toBlob()

语法如下:

canvas.toBlob(callback, mimeType, quality)
复制代码

如果看了6.3小节的同学应该会觉得,这2个方法长得差不多,参数也差不多,那么它们的效果是否也是差不多的呢?

当然,该方法的作用是将canvas画布上的信息转换为Blob对象。该方法接收的参数基本与6.3的方法相同,区别在于,该方法多接受一个参数,该参数为:

callback:Function;toBlob()方法执行成功后的回调方法,支持一个参数,表示当前转换的Blob对象。

6.3小节的最后说过,toDataURL()方法是同步方法,那么我们toBlob()与此不同,它是一个异步的方法,所以该方法会多接受一个参数callback,该参数就是toBlob()的回调函数。

好了,既然本文最终目的是将file压缩后,再转换成Blob对象上传至后端,那么为什么不直接使用toBlob()方法,而是使用toDataURL()方法呢?对于这点的解释我会在下一篇上传篇中进行详解,各位同学可以持续关注我的博客。

7.DataUrl格式

不知道有心的同学发现了吗?本文中多次提到了DataURL格式的数据,那究竟什么才是DataURL格式的数据呢?

在下面对DataURL展开了解之前,我们可以先来复习一下常见的img标签的src是什么样的?

那么src除了这种赋值方式之外,还有什么形式能够展示图片吗?

当然有,那就是我们的DataURL,详见下图:

可以看到,同样的图片,2种不同的src。那么DataURL在实际中有什么用处呢?它的定义是什么?什么场景下需要用到它?带着这个疑问看下去吧。

7.1 DataURL格式的定义

先来看看我从网上找到的比较官方的定义:DataURL是由RFC2397定义的一种把小文件直接嵌入文档的方案。格式如下:

其实整体可以视为三部分,即声明:参数+数据,逗号左边的是各种参数,右边的是数据。举个例子:

<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAS...">
复制代码

我们将例子与格式一一对照来看

MIME type:表示数据呈现的格式,简单来说就是这个数据的类型是什么?是png还是jpg甚至是html,结合实例来看,在实例中我们的这段数据它的类型就是一个图片,而且是个jpg格式的图片。

character:字符集;这个是可选项,默认为charset=US-ASCII,如果指定的是图片的话,就不再使用字符集了。在实例中,我们的这段数据代表的就是一张图片,随意是没有这个字段的。

base64:这一部分将表明数据编码方式,当然我们可以不使用base64的编码方式,那样我们将使用标准的URL编码方式。在本实例中,我们的图片就是采取base64的编码方式。

enconded data:这就是实际的数据了,在实例中,这就是这张图片的base64编码。

好了,DataURL的定义讲完了,那么DataURL只能在图片里使用吗?当然不是,它能表示的东西有很多很多,比如你们可以复制下面的这段代码到浏览器地址栏中粘贴看一看

data:text/html;charset=UTF-8,<html><body><p>欢迎看刘伟C的博客</p></body></html>
复制代码

下面会讲下DataURL的优缺点。

7.2DataURL的优缺点

这里就不做铺垫直接说

优点:

  1. 当我们访问外部资源很麻烦的时候比如跨域限制的时候,可以直接使用DataURL,因为他不需要向外界发起访问。
  2. 如果看过我Chrome调试工具NetWork模块这篇博客的朋友可能会注意到,浏览器都有默认的同一时间最多同时加载的资源数量限制,比如Chrome,最多允许同时加载6个资源,其它起源就得排队等待,但是对于DataURL来说,它并不需要向服务器发送Http请求,它就自然不会占用一个http会话资源。
  3. 当图片是在服务器端用程序动态生成,每个访问用户显示的都不同时(emmm,这个我也不是很理解,目前暂时没碰到过这种场景)

缺点:

  1. 有心的同学如果去尝试就会知道,base64的数据体积实际上会比原数据大,也就是Data URL形式的图片会比二进制格式的图片体积大1/3。所以如果图片较小,使用DataURL形式的话是比较有利的,图片较小即使比原图片大一些也不会大很多,相比发起一个Http会话这点开销算什么。但是如果图片较大的情况下,使用DataURL的开销就会相应增大了,具体如何取舍还得各位同学结合实际场景考量。
  2. Data URL形式的图片不会被浏览器缓存,这意味着每次访问这样页面时都被下载一次。这是一个使用效率方面的问题——尤其当这个图片被整个网站大量使用的时候。(这个缺点可以规避,具体可以自行百度或看其它的博客,这里不细说)

那么总结一下,DataURL带来的便利原因就是一个:它不需要发起http请求;而它的缺点归纳起来就是两个:体积比原有还要大不会被缓存

7.3 本文为何使用DataURL

看完上面的科普,同学们是不是都了解了DataURL的优缺点,那么不知道各位是否会好奇,本次前端压缩为何使用的是DataURL呢?

答案其实在第一小节中有,因为我们想做图片预览,而图片预览是通过FileReader对象实现的,FileReader会去读文件,并且返回文件的内容,但是返回的内容要么就是二进制字符串,要么就是二进制字节数组,这些都不能直接用于图片展示,只有调用FileReader.readAsDataURL()返回的DataURL格式的数据可以直接使用。

8. 代码

因为公司使用的前端框架比较非主流,是layUI框架的,所以我的代码也是基于layUI的前端压缩(所以你们直接copy我的代码大概率是跑不起来的),不过核心思想核心方法核心原理在博客中都有体现,只要理解了思想,其实不难将它复刻到你们自己的项目中去。

代码如下:

//这是layUI的文件上传组件,不了解的朋友也没必要去看文档,理解思想就行了
var upload1 = upload.render({
        elem: '.uploadImg',
        url: releaseUrl,
        accept: 'images',
        auto: false,
    	//选择文件后的钩子函数
        choose: function (obj) {
            var that = this;
            //预读本地文件示例,不支持ie8
            obj.preview(function (index, file, result) {
                console.log("选中的文件:", file);
                var index = layer.load(1, {
                    content: '图片上传中....',
                    shade: 0.2,
                    success: function (layer) {
                        layer.find('.layui-layer-content').css({
                            'paddingTop': '40px',
                            'width': '60px',
                            'textAlign': 'center',
                            'backgroundPositionX': 'center'
                        });
                    }
                });
                //这段开始才是压缩的重点,所以关注这段代码即可
                //若图片超过1M则启动压缩
                if (file.size > (1024 * 1024)) {
                    // console.log("大于1m");
                    canvasDataURL(file, that, function (blob) {
                        // console.log("压缩后的二进制Blob对象:",blob);
                        console.log("压缩前:" + (file.size / 1024 / 1024) + "M");
                        console.log("压缩后:" + (blob.size / 1024 / 1024) + "M");
                        var compresFile = new File([blob], file.name, {
                            type: file.type
                        })

                        obj.upload(1, compresFile);

                    })
                }
                //若图片不超过1M则无需压缩,直接传
                else {
                    var picDom = $(that.item).find("img");
                    picDom.attr('src', result);
                    obj.upload(1, file);

                }
            });
        },
    })
	//下面是压缩部分的核心代码
	/**
     * 通过canvas画布实现压缩,并转化为base64格式的图片
     * @param {File} file : 图片
     * @param {Object} item :通过item找到当前对象的img标签
     * @param {Function} callback :回调函数
     */
    function canvasDataURL(file,item,callback) { //压缩转化为base64
        var reader = new FileReader();      //读取文件的对象
        reader.readAsDataURL(file);         //对文件读取,读取完成后会将内容以base64的形式赋值给result属性
        reader.onload = function (e) {      //读取完成的钩子
            const img = new Image();
            const quality = 0.2; // 图像质量
            //先创建canvas画布,再获取canvas画布上的2d绘图环境,通过这个2d绘图环境才可使用绘制API
            const canvas = document.createElement('canvas');        //创建canvas画布
            const drawer = canvas.getContext('2d');                 //返回一个在画布上绘制2d图的环境对象,该对象上包含有canvas绘制2d图形的API
            img.src = this.result;
            // console.log("FileReader对象:",this);
            //图片预览
            var picDom = $(item.item).find("img");  
            picDom.attr('src', this.result); //图片链接(base64)
            //图片压缩代码,需要注意的是,img图片渲染是异步的,所以必须在img的onlaod钩子中再进行相应操作
            img.onload = function () {
                canvas.width = img.width;
                canvas.height = img.height;
                drawer.drawImage(img, 0, 0, canvas.width, canvas.height);
                convertBase64UrlToBlob(canvas.toDataURL(file.type, quality), callback);
            }
        }
    }
    
	//下面是上传部分的核心代码
    /**
     * 将base64格式转化为Blob格式
     * @param {string} urlData : urlData格式的数据,通过这个转化为Blob对象
     * @param {Function} callback : 回调函数
     */
    function convertBase64UrlToBlob(urlData, callback) { //将base64转化为文件格式
        // console.log("压缩成base64的对象:",urlData);
        const arr = urlData.split(',')
        // console.log("arr",arr);
        const mime = arr[0].match(/:(.*?);/)[1]
        const bstr = atob(arr[1])   //atob方法用于解码base64
        // console.log("将base64进行解码:",bstr);
        let n = bstr.length
        const u8arr = new Uint8Array(n)
        while (n--) {
        u8arr[n] = bstr.charCodeAt(n)
        }
        // console.log("Uint8Array:",u8arr);
        callback(new Blob([u8arr], {
        type: mime
        }));
    }
复制代码

9. 总结

关于前端图片压缩的压缩篇告一段落,下一篇博客将介绍前端图片压缩的上传篇,重点是

  1. Blob对象
  2. Base64转Blob的方法
  3. Uint8Array
  4. 为什么要先压缩为Base64(DataURL)?再将Base64(DataURL)转化为Blob对象?)

也欢迎大佬在评论区留言指正博客错误的地方,也欢迎像我一样的小白前端一起在评论区讨论。

10.参考文章

  1. 玩转图片流
  2. MDN_FileReader相关文档
  3. canvasApi中文网
  4. DataURL与base64