案例

某企业举行一次H5晒单活动,活动规定每个用户只要在该H5应用里面上传自己的购物小票清单,就可以获得一次抽奖的机会,晒单成功后,用户的购物小票会在应用首页展示出来。

活动持续3天,开发组遇到了各种稀奇古怪的问题,比如图片上传耗费时间长,并且容易失败,首页图片列表打开变慢,服务器容量不足等等。后来经过定位,对前端上传图片进行了压缩,问题一下子骤减。

今天本章节的主要内容就是关于图片处理

一、为什么要对图片进行处理

1.1 可以快速上传,增强用户体验

目前手机质量越来越好,相机拍照质量也越来越高,导致直接后果是手机端图片变大,随便一张图2-3M,那么如果上传这样的图片,用户所需时间将会增加,以1秒钟300kb计算,一张图所需时间越为10秒钟,用户耗费大量的时间在等待当中,以至于失去耐性,这对用户是极大的伤害。如果对图片进行压缩,比如压缩至300kb,用户所需时间将缩短为1秒,可以显著提高用户体验

1.2 可以降低流量

主要有俩个方面,一方面是用户上传的流量,1张3M的和一张300kb的,显而易见,前者消耗的流量大得多。另一方面是降低用户浏览图片所耗费的流量。

1.3 可以降低存储所用空间

经过压缩处理的图片,存储空间大大降低。

1.4 可以让应用更流畅

如果我们未对图片的显示效果做处理,当每个图片较大时,浏览器将超负荷渲染图片,该行为直接导致页面加载速度变慢,刷新响应变慢,甚至出现卡顿现象,另外手机压缩变慢。而对图片进行压缩处理后,应用将反应快,更加流畅

二、对图片处理的一般思路

2.1 利用cavas 对图进行缩放

cavas 有一个方法,利用该方法可以返回dataurl,该dataurl为经过base64编码后的图片内容,部分浏览器支持直接显示

canvas.toDataURL(type, encoderOptions);
type 可选图片格式,默认为 image/png ,jpg 为 image/jpeg
encoderOptions 可选在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量
如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
返回值:类似 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby
// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"      其中image/png 表示png图片类型,base64,         为固定参数标识这是base64编码

比如上例中展示即是如图所示的白色图标

这里有一个细节需要澄清,一般对于size 小于100kb的图片,经过该方法处理后存储得到的图片将大于100kb,也就是所谓的越压越大,所以最小先行判断,代码结构如下

该函数代码结构可以精简成如下所示

function  task_compress_and_then_upload(file,compressoption){

//第一步压缩    task_compress(file,compressoption)

//第二步上传     task_upload(result.data,compressoption.serveurl,compressoption.postarg,compressoption.base64key)

}

2.2 前端压缩的的核心函数

我们来看看核心代码 task_compress

//压缩核心函数
//科普一下File对象
/*
{lastModified:1511236246031
lastModifiedDate:”Tue Nov 21 2017 11:50:46 GMT+0800 (中国标准时间) “
name:”0.首页 – 副本.png”
size:2552293 //文件大小
type:”image/png” //文件类型
webkitRelativePath:””
}*/

//其中需要重点阐述的参数 compressoption

/**
* 上传并压缩核心代码,依赖jquery
*@param file : 需要上传的文件对象
* @param compressoption:压缩和上传参数,是一个对象
* {maxWidth:int,压缩后图片的最大宽度,默认400
* maxHeight:int,压缩后图片的最大高度,默认400
* quality:float,压缩图片质量,0-1之间小数,默认0.92
* serveurl:string,后端处理上传的连接地址,
* minSize:int,小于这个大小的文件都不压缩,
* onpickup:function,当文件选中即进行的回调,我们一般使用 e.target.result
* 这是该文件的base64编码,
* 类似于这样,data:image/png;base64,iVBORw0
* 我们可以使用用来做预览,
* postarg:{}//用户自定义的上传参数,如用户id等
* base64key:string,后端接收base64文件编码参数的名字,默认为base64data
* }
* @result:该函数封装了jquery 的promis 对象,因此采用then的方式,无返回
*
**/
function task_compress(file,compressoption){
//定义promis
vardeferred=$.Deferred();
//过滤一下,
if (file.type.indexOf(“image”) ==-1) {
deferred.resolve({“status”:400,”msg”:”当前文件类型暂不支持”})
returndeferred;
}
//用文件流对象读取文件
varreader=newFileReader();
varimage=newImage();
//定义流对象事件
reader.onload=function(e) {
//小于200kb的都不压缩
if(typeofcompressoption.onpickup==”function”){
compressoption.onpickup(e)
}
if(file.size<compressoption.minSize||200*1024){
deferred.resolve({“status”:200,”data”:e.target.result,”msg”:””})
returndeferred;
}else{
//大于200kb的先预览到图片上
image.src=e.target.result;
}
}
//开始读取事件
reader.readAsDataURL(file);
// 缩放图片需要的canvas
varcanvas=document.createElement(‘canvas’);
varcontext=canvas.getContext(‘2d’);
// 定义image 事件
//base64地址图片加载完毕后
image.onload=function () {
console.log(“image on load”)
// 图片原始尺寸
varoriginWidth=this.width;
varoriginHeight=this.height;
// 最大尺寸限制
varmaxWidth=compressoption.maxWidth||400, maxHeight=compressoption.maxHeight||400;
// 目标尺寸
vartargetWidth=originWidth, targetHeight=originHeight;
// 图片尺寸超过400×400的限制
if (originWidth>maxWidth||originHeight>maxHeight) {
if (originWidth/originHeight>maxWidth/maxHeight) {
// 更宽,按照宽度限定尺寸
targetWidth=maxWidth;
targetHeight=Math.round(maxWidth* (originHeight/originWidth));
} else {
targetHeight=maxHeight;
targetWidth=Math.round(maxHeight* (originWidth/originHeight));
}
}
// canvas对图片进行缩放
canvas.width=targetWidth;
canvas.height=targetHeight;
// 清除画布
context.clearRect(0, 0, targetWidth, targetHeight);
// 图片压缩
context.drawImage(image, 0, 0, targetWidth, targetHeight);
// canvas压缩并上传
vardataURL=canvas.toDataURL(compressoption.mime||”image/jpeg”,compressoption.quality||0.92);
deferred.resolve({“status”:200,”data”:dataURL,”msg”:””})
};
returndeferred
}

2.3 前端上传的的核心函数

上传采用ajax 上传

/**
* @param dataurl:文件的base64编码
* @param serveurl:后端接收上传的地址
* @param postarg:后端接收的其他参数
*@param base64key:后端接收base64编码的参数名称
**/
function task_upload(dataurl,serveurl,postarg,base64key){
vardeferred=$.Deferred();
vardata= {}
for(variinpostarg||{}){
data[i] =urldecode(postarg[i]);
}
data[base64key||”base64data”]=result
$.ajax({
“url”:serveurl||”/attach/upload”,
async:true,
method:”post”,
data:data,
success:function(result){
deferred.resolve(result)
},
error(xhr,status,error){
deferred.resolve({“status”:400,”msg”:”服务器繁忙请稍后再试”})
}
});
returndeferred
}

2.4 关于Promis

在上述代码中我们可以看见大量地采用了的形式,

somefunction().then()

实际上回调函数也可以解决这些问题,但是由于如下俩点,我们决定采用promis

  • 回调函数中 return 和 throw 这些关键字几乎失效。
  • 回调函数使得我们代码变得复杂逻辑混乱

Promise 对象有以下两个特点。

1、对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

实现promise的框架很多,典型的如

  • q:       https://github.com/kriskowal/q
  • when:  https://github.com/cujojs/when

jquery也有简单的实现,其中$.Deferred实现了Promise规范,then、done、fail、always是Deferred对象的方法。$.when是一个全局的方法,用来并行运行多个异步任务,与ES6的all是一个功能。ajax返回一个Deferred对象,success、error、complete是ajax提供的语法糖,功能与Deferred对象的done、fail、always一致。

2.5 关于后端处理逻辑

本文后端采用go语言实现

func (ctrl *AttachCtrl) upload(ctx *gin.Context) {
    base64data := ctx.PostForm(“base64data”)
    if strings.Contains(base64data, “base64,”) {
        datastrarr := strings.Split(base64data, “base64,”)
        base64data = datastrarr[1]
    }
    ddd, error := base64.StdEncoding.DecodeString(base64data) //成图片文件并把文件写入到buffer
    if error != nil {
        ctx.JSON(http.StatusOK, gin.H{“status”: 400, “data”: error, “msg”: “”})
        return
    }
    timestamp := strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
    filename := “/upload/” + timestamp + “.jpg”
    err2 := ioutil.WriteFile(“.”+filename, ddd, 0666) //buffer输出到jpg文件中(不做处理,直接写到文件)
    if err2 == nil {
        ctx.JSON(http.StatusOK, gin.H{“status”: 200, “data”: filename, “msg”: “”})
    } else {
        ctx.JSON(http.StatusOK, gin.H{“status”: 400, “data”: err2, “msg”: “文件存储出错”})
    }
}

我们将go语言自己封装成一个框架,需要该框架的可以去我的开源里面看看

2.6、封装为jquery插件

$.fn.compressandupload=function(compressoption){
vardeferred=$.Deferred();
$(this).change(function(){
varfile=$(this).get(0).files[0]
task_compress_and_then_upload(file,compressoption).then(function(result){
deferred.resolve(result)
})
})
returndeferred
}
注意,该函数只支持单个dom绑定
如<input type=”file” name=’file” id=”fileid” />
function onpickup(e){
console.log(“onpickup”,e)
$(“#prev”).attr(“src”,e.target.result)
}
$(function(){
$(“#filedom”).compressandupload({
onpickup:onpickup,
“maxWidth”:200,
maxHeight:200,
quality:0.1,
serveurl:”/attach/upload”
}).then(
function(result){
console.log(“result”,result)
})
})

3、演示效果及demo

先上图

代码地址

https://github.com/techidea8/jquery.compressupload.js

如果需要后端代码,请加微信jiepool-winlion

发表评论

电子邮件地址不会被公开。 必填项已用*标注