为了账号安全,请及时绑定邮箱和手机立即绑定

千万级流量 H5 应用涉及到图片处理技能点

标签:
Html5 CSS3 Go

最近开了一节课,《支持10万人同时在线 Go语言打造高并发web即时聊天(IM)应用》

https://img1.sycdn.imooc.com//5ce890400001051714480650.jpg

课程播出后,很多读者问到图片处理相关的东西,如怎么进行前端压缩,和异步上传等,有鉴于此,笔者系统性地整理了图片处理相关技术细节。老规矩,文章末尾给有源代码地址。


从 2017 年开始,我们持续为某企业支撑多场 HTML5 晒单赢红包活动,活动规则(套路)如下:

  1. 用户在商超里面购买产品 P,获得小票 T;

  2. 用户关注公众号,从菜单进入小票上传页面内,用手机拍摄小票,并上传;

  3. 上传成功后,后端通过图像识别,分辨小票内容是否包含产品 P,借此判断用户是否有抽奖的机会;

  4. 小票上传成功后,将在列表页面 L 中按照上传先后顺序显示。

整个活动持续运维 2 年多,整个过程遇到了各种奇葩问题,举例如下:

  1. 因为有些手机像素太高,拍照图片达到 2M 左右,上传太慢,上传出错;

  2. 部分手机上传后相片旋转了 90 度;

  3. 列表页面加载速度越来越慢,甚至卡顿;

  4. 图片铺满了硬盘空间,导致应用日志写入失败,系统报错;

  5. 用户反应页面打开慢,白屏;

  6. 多用户同时上传图片,有用户上传失败;

  7. 有用户反应手机无牌照弹窗;

  8. 经过分析反应页面打开慢的大部分是北方用户。

  9. ...... 本场 chat 的目的也就在此,希望从如何解决这些问题出发,举一反三,触类旁通,最后达到系统化地梳理图片应用常用方法和技巧的目的。

图片异步上传

2.1 异步上传的好处

异步上传图片的好处显而易见。首先用户能获得更好的用户体验,异步上传页面不需要要刷新,在上传过程中,应用可以通过进度条,loading 等效果,达到友好提醒的目的。其次,能将图片业务抽象,业务逻辑和图像上传解耦合。

2.2 用插件实现前端异步上传

常用插件 uploadify、webuploader、ajaxfileupload.js、jquery.form.js 等,这里不做重点讨论。

2.3 用 JS+H5 实现图片上传

2.3.1 JavaScript 代码段
//异步上传核心函数//filedom 通过dom query方法返回的dom如document.getElementByID("test")//onsuccess上传成功回//onerror上传失败回调function uploadfile(filedom,
onsuccess,
onerror,
onprogress ){//使用H5的formdata进行上传//formdata 是H5新增加内容var formdata = new FormData();  
formdata.append(filedom.name,filedom.files[0]);//还可以同时上传参数formdata.append("clientID","客户端的唯一标识"); //在AndroID/IOS 系统中webview都支持XMLHttpRequest//无需考虑IEvar xhr= new XMLHttpRequest();

xhr.upload.onprogress=console.log;//如果定义了进度函数if(typeof onprogress=="function"){
    xhr.upload.onprogress = onprogress;
}//如果定义了上传成功回调if(typeof onsuccess!="function"){
    onsuccess = console.log;
}//如果定义了上传失败回调if(typeof onerror!="function"){
    onerror = console.log;
}//第二个参数是后端服务地址,将做重点说明xhr.open("POST", "/attach/upload");//ajax发送数据xhr.send(formdata);//时间成功回调xhr.onreadystatechange = function(){  //OnReadyStateChange事件
        if(xhr.readyState == 4){  //4为完成
            if(xhr.status == 200){    //200为成功
                onsuccess(JSON.parse(xhr.responseText)) 
            }else{
                onerror(xhr) 
            }
        }
    };
}//上传成功后回调处理function     ajaxuploadsuccess(res){}//上传失败回调处理function     ajaxuploaderror(res){}//上传失败回调处理function      ajaxuploadprogress(ev) {                if(ev.lengthComputable) {                    var percent = 100 * ev.loaded/ev.total;                    console.log({"文件总大小":ev.total,"已上传":ev.loaded,"上传进度":percent+"%"});
                }
            }
2.3.2 Html 代码段
<input 
type="file" name="file" accept="image/*" capture="filesystem" onchange="uploadfile(this,ajaxuploadsuccess,ajaxuploaderror)">

特别需要注意

  1. name 属性即为后端接收文件参数的名字,不能缺失。

  2. accept 属性标识了该控件只能检索图片类型文件。

  3. capture=filesystem 属性标识了系统只能通过摄像头拍照。

2.3.3 让界面更漂亮

为了让界面变得更加美观,比如要实现如下按钮,点击头像即可上传效果

图片异步上传效果

我们通常需要对 DOM 进行特殊的处理

<li class="mui-table-view-cell mui-media">
<a class="mui-navigate-right">
<input 
onchange="uploadfile(this,ajaxuploadsuccess)" accept="image/png,image/jpeg" type="file"placeholder="请输入群名称" class="mui-input-clear mui-input" style="wIDth: 100%; height: 48px; position: absolute; opacity: 0;"> 
<img ID="head-img" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://images.gitbook.cn/5489db20-3466-11e9-93c1-37d3989d4cb2" class="mui-media-object mui-pull-right head-img" style="border-radius: 50%;"> 
<div class="mui-media-body">
                    头像
<p class="mui-ellipsis">点击右侧上传头像</p>
</div>
</a>
</li>

其中

  1. input 标签 opacity 属性设置为 0,意味着这个 input 是透明的,所以能看到下面的图片。

  2. input 标签 position 为 absolute,宽度和高度适当则刚好可以让这个 dom 遮住 image ,用户点击 image 附近任意位置都可以实现上传。

  3. 为了头像变成圆形,设置 img 标签 border-radius: 50%。

  4. 在上面的例子中,ajaxuploadsuccess 函数中实现了图片的 src 更新功能。

上传成功回调说明:

/*内容格式说明
{"code":0,"data":"https://images.gitbook.cn/5489db20-3466-11e9-93c1-37d3989d4cb2","msg":"ok"}
*/function ajaxuploadsuccess(res){
//上传成功后返回上传成功的图片地址,并更新用户头像地址
    document.getElementByID("head-img").src=res.data ;
}

图片压缩

3.1 为什么要对图片进行处理

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

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

3.1.2 可以降低流量

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

3.1.3 可以降低存储所用空间

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

3.1.4 可以让应用更流畅

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

3.2 实现前端压缩

3.2.1 关于 Html5 操作文件的基本知识
3.2.1.1 关于文件对象 File

如下是 console.log 打印出来的 file 对象内容。file 对象描述了文件的基本信息,但是没有文件内容。通过 input 标签可以获得 files 数组,遍历该数组可以获得具体每一个 file 对象。

var files = document.getElementByID("filedomID").files;for(var file in files){    console.log(file);
}

在 Chrome 引擎浏览器中效果

{
lastModified:1511236246031,//最近一次更新时间戳lastModifiedDate:"Tue Nov 21 2017 11:50:46 GMT+0800 (中国标准时间) ",//最近一次更新时间name:"0.首页 – 副本.png",//文件名称size:2552293,//文件大小,单位Bytetype:"image/png",//文件类型webkitRelativePath:""//input上加上webkitdirectory属性时,用户可选择文件夹,此时weblitRelativePath表示文件夹中文件的相对路径}

Firefox下对象信息如下,缺少了 lastModifiedDate 信息

{
lastModified:1511236246031,//最近一次更新时间戳name:"0.首页 – 副本.png",//文件名称size:2552293,//文件大小,单位Bytetype:"image/png",//文件类型webkitRelativePath:""}

IE 下打印信息如下

{constructor: File {...}, lastModifiedDate: "Tue Nov 21 2017 11:50:46 GMT+0800 (中国标准时间) ",//最近一次更新name: "0.首页 – 副本.png", size: 75605, type: "image/jpeg"}

可见无论何种浏览器,size、name、type 属性都会存在。我们将利用 file 的 size 属性做大小判断,name 属性做类型校验。

3.2.1.2 FileReader 对象简介

FileReader 对象提供了操作文件内容的接口

方法名称描述
readAsArrayBuffer(file)按字节读取文件内容,结果用ArrayBuffer对象表示
readAsBinaryString(file)按字节读取文件内容,结果为文件的二进制串
readAsDataURL(file)读取文件内容,结果用data:url的字符串形式表示
readAsText(file,encoding)按字符读取文件内容,结果用encoding编码的字符串形式表示
abort()终止文件读取操作

其中,readAsDataURL 方法可以将文件内容编码成 Base64 格式,这点很重要,这意味着我们可以用 Base64 编码统一 Canvas 图片压缩接口。

另外,FileReader提供了如下事件机制:

方法名称描述
onabort当读取操作被中止时调用
onerror当读取操作发生错误时调用
onload当读取操作成功完成时调用,一般使用用该方法进行回调
onloadend当读取操作完成时调用,无论成功或失败
onloadstart当读取操作开始时调用
onprogress在读取数据过程中周期性调用,进度回调
我们可以利用onload事件来处理文件内容。onprogress处理进度属性。
回调函数原型如下:
function(e){}

e 参数格式如下,我们可以通过 e.target.result 获得文件内容,如图所示,这是一连串 Base64 格式字符串。

enter image description here

3.2.1.3 获取文件内容

由以上可以获得读取文件内容的一般函数:

//file:这是一个文件对象
            //onload :加载成功回调
            //onerror 加载失败回调
            //onprogress :加载进度回调function filetobase64withfilereader(file,onload,onerror,onprogress){//创建对象
  var reader = new FileReader();  //发起请求
  reader.readAsDataURL(file);//发起异步请求
  //配置回调函数
  reader.onload=function(ev){      if(!!onload){
      onload({"code":200,"data":ev.target.result,"msg":""})
      }
  }  if(typeof onerror=="function"){
    reader.onerror = function(ev){
        onerror({"code":400,"data":ev,"msg":"加载文件出错"})
    };  
  }else{
      reader.onerror = console.log;
  }  if(typeof onprogress=="function"){
    reader.onprogress = onprogress;  
  }else{
      reader.onprogress = console.log;
  }
}
3.2.2 利用 Canvas 对图进行压缩
3.2.2.1 Canvas 实现图片压缩的原理

Canvas.toDataURL(type, encoderOptions);

利用该方法可以返回 dataUrl 数据,这是一连串经过 Base64 编码后的图片内容,这些内容在大部分浏览器中都能直接显示。函数参数说明如下:

  • 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 编码。上面所示字符串是一个白色图标。

3.2.2.2 实现 Canvas 压缩图片函数

综上所述,可以直接上代码了:

//压缩成功回调function oncompress(res){ console.log(res)                 document.getElementByID("testimg").src=res.data;
            }//报错回调function onerror(res) {                    console.log("onerror",res)
            }//这个函数的核心思路//首席按调用filereader对象获得文件内容base64格式//然后判断如果不需要压缩就返回base64//否则调研Canvas绘制图片,最后将Canvas上的内容导出为dataUrl格式,即base64格式//所有数据都通过回调函数传递function filetobase64withfilereader(file,onload,onerror,onprogress) {    var reader = new FileReader();    //发起请求
    reader.readAsDataURL(file);//发起异步请求
    //配置回调函数
    if (typeof onload == "function") {
        reader.onload = onload;
    } else {
        reader.onload = console.log;
    }    if (typeof onerror == "function") {
        reader.onerror = onerror;
    } else {
        reader.onerror = console.log;
    }    if (typeof onprogress == "function") {
        reader.onprogress = onprogress;
    } else {
        reader.onprogress = console.log;
    }
}//图片压缩后的最大宽度var CompressMaxWIDth = 400;//图片压缩后的最大高度var CompressMaxHeight = 400;//图片压缩后获取图片质量var CompressQuality=0.92;//如下函数核心逻辑如下//通过先用filereader对象加载file,获得文件内容//在filereader加载完成后的回调函数onload里面//可以将内容填充到一个Image对象中,//在Image加载完成时onload回调函数中,//调用Canvas,的draw方法,将图片内容加载到Canvas上,//同时,可以通过设置Canvas画布大小,实现图片缩放,//最后利用Canvas的toDataUrl方法,获得画布上的图片内容function filetobase64withCanvas(file,onsuccess,onerror){//这个方法只支持image方法console.log(file.type.indexOf("image/"));if(!file || file.type.indexOf("image/")==-1){
    onerror({"code":400,"msg":"不支持改格式"})    return ;
}var reader = new FileReader();//加载图片文件到base64 编码,image可以直接加载啦reader.readAsDataURL(file);//发起异步请求var image = new Image();
reader.onload = function(ev){
image.src = ev.target.result;
};// 缩放图片需要的Canvasreader.onerror = function(ev){
    onerror({"code":400,"data":ev,"msg":"读取文件出错"});
}
image.onerror = function(ev){
    onerror({"code":400,"data":ev,"msg":"展示图片出错"});
}var Canvas=document.createElement("Canvas");var context=Canvas.getContext("2d");//定义image 事件//base64地址图片加载完毕后image.onload=function () {        console.log("image",this);// 图片原始尺寸var originWIDth=this.wIDth;var originHeight=this.height;//期待的目标尺寸var targetWIDth=originWIDth, targetHeight=originHeight;//如果原始尺寸小于压缩的尺寸,说明是放大,不在我们这里处理的范围内。//如果原始尺寸大于压缩的尺寸,说明我们需要压缩。var needcompress =originWIDth>CompressMaxWIDth ||
originHeight>CompressMaxHeight;if(!needcompress){
    onsuccess({        "code":200,        "data":this.src,        "msg":"加载成功"
    })
} else{var orate =originWIDth/originHeight;//假设目标宽度高度都是最大值//则目标尺寸的比列如下var drate = CompressMaxWIDth/CompressMaxHeight;var k = orate/drate;//要想得到等比缩放的压缩效果,//如果k=orate/drate=1说明是等比缩放了不要处理//如果k=orate/drate>1说明当前设置的目标宽高比相比理想比例偏小,//要么增加宽度,要么降低高度,显然不能增加宽度,因此只能降低高度了//如果orate/drate<1说明当前设置的目标宽高比比相比理想比例偏大,//要么降低宽度,要么增加高度,显然不能增加高度,因此只能降低宽度了if (k>1){
     targetWIDth = CompressMaxWIDth;
      targetHeight= Math.round(CompressMaxHeight/k);
} else {
    targetHeight=CompressMaxHeight;
    targetWIDth=Math.round(CompressMaxWIDth* k);
}//Canvas对图片进行缩放Canvas.wIDth=targetWIDth;
Canvas.height=targetHeight;// 清除画布context.clearRect(0, 0, targetWIDth, targetHeight);// 图片压缩
 context.drawImage(image, 0, 0, targetWIDth, targetHeight);// Canvas压缩并上传var dataURL=Canvas.toDataURL(file.type,CompressQuality);//成功回调onsuccess({"code":200,"data":dataURL,"msg":""})
    }
   }
}

测试 Html 代码如下

 <input 
 type="file"
  name="file" onchange="filetobase64withCanvas(this.files[0],oncompress,onerror)"><img class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="" ID="testimg"/>    }

效果展示如下为压缩后的图片,该图模糊不清。大小 15kb 。

enter image description here 如下为未压缩的图片,显然该图清晰可见。大小 128kb 。 enter image description here

3.2.2.3 并非所有图片都适合用 Canvas 方式进行压缩

如下几个细节需要澄清:

  • size 小于 100kb 的图片不适合用 Canvas 进行压缩,经过该方法处理后存储得到的图片将大于 100kb。

  • Canvas 压缩会导致图片失真。如果我们的图片是票据等,不适合用改方法进行压缩。

  • 对 Canvas 方法进行处理图片时,我们应该先行判断,该图片是否适合压缩。

使用 Canvas 进行压缩,伪代码如下:

function  compress(filedom,onsuccess,onerror){    //定义变量
    var file = filedom.files[0];    if(file.size<1024*100){    //使用filereader将文件转成base64,然后传入onsuccess
     filetobase64withfilereader(file,
     onsuccess,
     onerror);
    }else{    //使用Canvas 将文件内容转成base64字符串,然后传入onsuccess
    filetobase64withCanvas(file,onsuccess,onerror);
}
}
3.2.2.4 上传 Base64 格式数据注意事项

通过以上我们得到了 Base64 格式内容,接下来要做的就是将该内容上传到后端,但是因为 Canvas 或者 FileReader 得到的 Base64 格式字符串中存在特殊字符,因此上传前需要做相应转换,否则将会导致后端保存的图片报错。解决这些问题的方法很多,这里推荐一种方法,就是采用 encodeURIComponent(data) 函数对 Base64 字符串内容进行预处理。后端接收到后通过类似 urIDecode 的方法进行解密,最后保存为图片文件。 简单代码如下,本代码采用 x-www-form-urlencoded 格式发送,后端 ContentType 请使用相应的方式。

function uploadbase64(url,base64data,onsuccess,onerror){var xhr= new XMLHttpRequest();var data ={}//base64编码预处理data.base64data=base64data;//如果定义了上传成功回调if(typeof onsuccess!="function"){
    onsuccess = console.log;
}//如果定义了上传失败回调if(typeof onerror!="function"){
    onerror = console.log;
}//第二个参数是后端服务地址,将做重点说明xhr.open("POST", url);//ajax发送数据var postdata=[];for(var i in data){
    postdata.push(i+"="+encodeURIComponent(data[i]));
}
postdata.join("&");//注意下面该函数的位置,在xhr.open之后才起作用。xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(postdata.join("&"));//时间成功回调xhr.onreadystatechange = function(){  //OnReadyStateChange事件
        if(xhr.readyState == 4){  //4为完成
            if(xhr.status == 200){    //200为成功
                onsuccess(JSON.parse(xhr.responseText)) 
            }else{
                onerror(xhr) 
            }
        }
    };
}

3.3 在后端代码层实现压缩

正如以上所述,前端压缩虽然能实现压缩,但是导致像素失真,很多关键信息都丢失了。当时我们采用前端压缩,小票信息失真,导致图像识别准确率大大降低。有鉴于此,我们将目光转向了后端压缩技术。

3.3.1 Java 实现后端压缩

Java 后端压缩方法很多, Graphics 类的 drawImage 方法可以实现压缩,也有采用开源包的。这里采用开源包 net.coobird.thumbnailator。核心代码如下:

Thumbnails.
of(sourcefilepath).
scale(rate).
outputQuality(quality).
toFile(destfilepath);

相关参数说明如下:

  • sourcefilepath 源文件图像地址。

  • destfilepath 缩略图地址。

  • rate 图片压缩比列。

  • quality 输出图片质量。

使用该包需要引入依赖项,以 Maven 为例:

<dependency>
      <groupID>net.coobird</groupID>
      <artifactID>thumbnailator</artifactID>
      <version>0.4.8</version></dependency>

Java测试代码如下

//app.javapackage com.imwinlion.thumb;import java.io.IOException;import net.coobird.thumbnailator.Thumbnails;public class ThumbApplication {    public static voID main(String[] args) throws IOException{

        System.out.println(args.length);        if(args.length!=2){
            System.out.println("jave -jar app.jar src.jpg dst.jsp");
        }else{
            Thumbnails.of(args[0]).scale(0.5f).outputQuality(0.6).toFile(args[1]);
        }
}

命令行参数

enter image description here

其中,rate=0.5、quality=0.6,裁剪后的效果如下:

enter image description here

对比前端上传,显然效果清晰。

3.3.2 Golang 实现后端压缩

Golang 后端压缩需要引入第三方包如下

import (
    "image"
    "os"
    "image/gif"
    "errors"
    "image/jpeg"
    "image/png"
    "github.com/nfnt/resize"
)

其中 github.com/nfnt/resize 是一个第三方包,里面封装了大部分常用的图片操作工具类。

Golang 实现后端压缩核心代码如下

//srcpath:源文件路径,
// dstpath:缩略图路径
//dstMaxW:缩略图最大宽度
//dstMaxH:缩略图最大高度
func thumb(srcpath,dstpath string,dstMaxW,dstMaxH int)(err error) {
    //打开源图
    file, err := os.Open(srcpath)
    defer file.Close()
    if err!=nil{
        return
    }
    //获得图片对象,以及图片格式等
    origin, fmtimg, err := image.Decode(file)
    if err!=nil{
        return
    }
    bounds := origin.Bounds()
    //原始图片宽度
    srcw := bounds.Max.X
    //原始图片高度
    srch := bounds.Max.Y

    //得到原始宽高比和给定参数的宽高比
    k := (srcw / srch) / (dstMaxW / dstMaxH)
    targetW := dstMaxW
    targetH := dstMaxH
    //如果k>1 说明 dstMaxW/dstMaxH 偏小
    //那么 dstmaxh应该缩小(dstMaxw不能增大了)
    //如果k<1 说明 dstMaxW/dstMaxH 偏大
    //那么 dstMaxW 应该缩小(dstMaxH不能增大了)
    if (k > 1) {
        targetH = dstMaxH / k
    } else {
        targetW = dstMaxW / k
    }

    //然后采用图片压缩
    out, _ := os.Create(dstpath)
    defer out.Close()
    rect := image.Rect(0,0, targetW, targetH)
    switch fmtimg {
    case "jpeg": //jpg 格式
        img := origin.(*image.YCbCr)
        subImg := img.SubImage(rect).(*image.YCbCr)
        return jpeg.Encode(out, subImg, &jpeg.Options{Quality:100*targetW/srcw,})
    case "png": //png 格式
        Canvas := resize.Thumbnail(uint(targetW), uint(targetH), origin, resize.Lanczos3)
        switch Canvas.(type) {
        case *image.NRGBA:
            img := Canvas.(*image.NRGBA)
            subImg := img.SubImage(rect).(*image.NRGBA)
            return png.Encode(out, subImg)
        case *image.RGBA:
            img := Canvas.(*image.RGBA)
            subImg := img.SubImage(rect).(*image.RGBA)
            return png.Encode(out, subImg)
        }
    case "gif": //gif 格式
        img := origin.(*image.Paletted)
        subImg := img.SubImage(rect).(*image.Paletted)
        return gif.Encode(out, subImg, &gif.Options{})
    //用户可以添加bmp格式支持,需要安装golang.org/x/image/bmp
    /*
    case "bmp":
        img := origin.(*image.RGBA)
        subImg := img.SubImage(rect).(*image.RGBA)
        return bmp.Encode(out, subImg)
    */
    default:
        return errors.New("ERROR FORMAT")
    }
    return nil
}

检验代码如下

func  main()  {
    thumb("src.png","dst.png",930,500)
}

压缩得到的效果图,和 Java 效果一致。 enter image description here需要注意的是,如果需要添加 BMP 支持,需要安装 BMP 操作类。 golang.org/x/image/bmp,因为某些原因,该包不能正常安装,解决方法是在 gopath/src 目录下 新建文件夹 golang.org\x 如下所示 enter image description here 然后执行 clone 操作

>mkdir -p golang.org\x>cd golang.org\x>git clone https://github.com/golang/image.git

事实上很多扩展包都可以通过此方法安装,如 protobuf

>git clone https://github.com/golang/net.git>git clone https://github.com/golang/text.git>git clone https://github.com/golang/protobuf.git
3.3.3 使用 PHP 进行压缩

PHP 图片压缩使用的核心函数

imagecreatefromxxx($filepath)

表示创建一块画布,并从$filepath路径处,载入一副 xxx 类型的图像。常用的文件处理函数如下

imagecreatefromjpeg(filepath)
imagecreatefrompng(filepath)
imagecreatefromwbmp(filepath)
imagecreatefromgif(filepath)

PHP 图片压缩的另俩个核心函数 imagecopyresized 和 imagecopyresampled ,这俩个函数都是用来缩放的,但是都有缺陷,imagecopyresampled 得到的图片偏大,imagecopyresized 得到的图片质量较差。

bool imagecopyresampled ( 
resource $dst_image , 
resource $src_image , 
int $dst_x , 
int $dst_y , 
int $src_x , 
int $src_y , 
int $dst_w , 
int $dst_h ,int $src_w , 
int $src_h 
)
bool imagecopyresized ( 
resource $dst_image , 
resource $src_image , 
int $dst_x , 
int $dst_y , 
int $src_x , 
int $src_y , 
int $dst_w , 
int $dst_h ,int $src_w , 
int $src_h 
)

参数说明如下:

$dst_image:新建的图片。$src_image:需要载入的图片。$dst_x:设定需要载入的图片在新图中的x坐标。$dst_y:设定需要载入的图片在新图中的y坐标。$src_x:设定载入图片要载入的区域x坐标。$src_y:设定载入图片要载入的区域y坐标。$dst_w:设定载入的原图的宽度(在此设置缩放)。$dst_h:设定载入的原图的高度(在此设置缩放)。$src_w:原图要载入的宽度。$src_h:原图要载入的高度

PHP 压缩文件一般流程如下

$filename="src.jpg";//创建一幅图片对象$src_image=imagecreatefromjpeg($filename);//获得图片的原始宽度和高度list($src_w,$src_h)=getimagesize($filename);//设置缩放比例$scale=0.5;//获得压缩后的图片宽度和高度,这个也可以借鉴golang例子自动计算获得。$dst_w=ceil($src_w*$scale);
$dst_h=ceil($src_h*$scale);//创建一块画布$dst_image=imagecreatetruecolor($dst_w, $dst_h);//把源图片画到新创建的画布上imagecopyresampled($dst_image, $src_image, 0, 0, 0, 0, $dst_w, $dst_h, $src_w, $src_h);//设置输出格式header("content-type:image/jpeg");//输出图片imagejpeg($dst_image);//释放内存空间imagedestroy($src_image);
imagedestroy($dst_image);

我们封装函数如下:

//$srcpath:源文件路径,//$dstpath:缩略图路径//$dstMaxW:缩略图最大宽度//$dstMaxH:缩略图最大高度//$method:默认的图片压缩方式.imagecopyresampledfunction thumb($srcpath,$dstpath,$dstMaxW,$dstMaxH,$method="sampled"){    /*
getimagesize返回说明
索引 0 给出的是图像宽度的像素值
索引 1 给出的是图像高度的像素值
索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM
索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 <image> 标签
*/
    list($srcW, $srcH, $type, $attr) = getimagesize($srcpath);
    $imageinfo = array(                     'wIDth'=>$srcW,                     'height'=>$srcH,                     'type'=>image_type_to_extension($type,false),                     'attr'=>$attr
              );
    $imagetype =        $imageinfo['type']; 
    //获得处理函数
    $fun = "imagecreatefrom".$imagetype;    //获得原始图片
    $origin = $fun($srcpath);    //得到原始宽高比和给定参数的宽高比
    $k = ($srcW / $srcH) / ($dstMaxW / $dstMaxH);
    $targetW = $dstMaxW;
    $targetH =  $dstMaxH;    //如果k>1 说明 dstMaxW/dstMaxH 偏小
    //那么 dstmaxh应该缩小(dstMaxw不能增大了)
    //如果k<1 说明 dstMaxW/dstMaxH 偏大
    //那么 dstMaxW 应该缩小(dstMaxH不能增大了)
    if ($k > 1) {
        $targetH = $dstMaxH /$k;
    } else {
        $targetW = $dstMaxW /$k;
    }
    $thump = imagecreatetruecolor($targetW,$targetH);              //将原图复制带图片载体上面,并且按照一定比例压缩,极大的保持了清晰度
    $copyfun =  "imagecopyresampled";    if($method=="resized"){
        $copyfun = "imagecopyresized";
    }
    $copyfun($thump,$origin,0,0,0,0,$targetW,$targetH,$srcW,$srcH);

    $funcs = "image".$imagetype;
    $funcs($thump,$dstpath);
    imagedestroy($origin);
    imagedestroy($thump);
}    
//测试代码如下thumb("./src.png","resized.png",930,500,"resized");
thumb("./src.png","resampled.png",930,500,"resampled");

如下是我我们用 imagecopyresized 得到的效果图,大小 38kb,质量非常模糊。 enter image description here如下使我们用 imagecopyresampled 得到的效果图,大小为 178kb,原图大小 128kb。 enter image description here

显然 imagecopyresampled 质量优于 imagecopyresized。 需要注意的是,PHP 图像处理需要开启 GD 库支持。具体操作,如下:

打开 PHP.ini 文件中可以加载 GD 库,可以在 PHP.ini 文件中到如下扩展,

;extension=PHP_gd2.dll

将选项前的分号删除,保存,再重启 Apache 服务器即可。

上面所述都是利用应用层代码实现压缩,实际上我们可以在服务器层面进行压缩,比如 Nginx 服务器,可以扩展图片压缩模块。

3.4 自建图片服务器进行压缩

Nginx 服务器可以扩展图片处理模块,它和需要缩略图机制的应用场景非常契合。该服务器一般与缓存配合使用。

3.4.1 安装模块

编译前请确认您的系统已经安装了libcurl-dev libgd2-dev libpcre-dev 依赖库

#Debian / Ubuntu 系统举例# 如果你没有安装GCC相关环境才需要执行$ sudo apt-get install build-essential m4 autoconf automake make 
$ sudo apt-get install libgd2-noxpm-dev libcurl4-openssl-dev libpcre3-dev#CentOS /RedHat / Fedora举例# 请确保已经安装了gcc automake autoconf m4 #$ sudo yum install gd-devel pcre-devel libcurl-devel 支持Nginx和Tengine,两者选其一# 下载Nginx$ wget http://nginx.org/download/nginx-1.4.0.tar.gz#解压$ tar -zxvf nginx-1.4.0.tar.gz
$ cd nginx-1.4.0#下载 图片压缩模块$ wget https://github.com/oupula/ngx_image_thumb/archive/master.zip#解压$ unzip master.zip#配置$ ./configure --add-module=./nginx-image-master#编译$ make#安装$ sudo make install
3.4.2 设置配置文件

打开 Nginx 配置文件nginx.conf

vim /etc/nginx/nginx.conf

不同的系统该文件路径不一样,请按照自己的系统为准。

location / {   root html;   #添加以下配置
   image on;   image_output on;
}

或者指定目录开启

location /mnt {   root html; 
   image on;   image_output on;
}
3.4.3 参数说明

image on/off:是否开启缩略图功能,默认关闭。 imagebackend on/off:是否开启镜像服务,当开启该功能时,请求目录不存在的图片(判断原图),将自动从镜像服务器地址下载原图。 imagebackendserver:镜像服务器地址。 imageoutput on/off:是否不生成图片而直接处理后输出,默认 off。 imagejpegquality 75:生成 JPEG 图片的质量默认值 75。 imagewater on/off:是否开启水印功能。 imagewatertype 0/1:水印类型 0,图片水印 1,文字水印。 imagewatermin 300 300:图片宽度 300 高度 300 的情况才添加水印。 imagewaterpos 0-9:水印位置默认值9,0为随机位置,1为顶端居左,2为顶端居中,3为顶端居右,4为中部居左,5为中部居中,6为中部居右,7为底端居左,8为底端居中,9为底端居右。 imagewaterfile: 水印文件(jpg/png/gif),绝对路径或者相对路径的水印图片。 imagewatertransparent: 水印透明度,默认 20,越小越透明,0最透明 。 imagewatertext:水印文字 "Power By Vampire"。 imagewaterfontsize:水印大小 默认 5 。 imagewaterfont: 文字水印字体文件路径。 imagewatercolor: 水印文字颜色,默认 #000000 。

3.4.4 调用说明

这里假设你的 Nginx 访问地址为 http://localhost/,并在 Nginx 网站根目录存在一个 test.jpg 的图片。通过访问http://localhost/test.jpg!c300x200.jpg,将会 生成或输出一个 300x200 的缩略图。其中 300 是生成缩略图的宽度,200 是生成缩略图的 高度。一共可以生成四种不同类型的缩略图。支持 jpeg/png/gif (Gif生成后变成静态图片)。

  • C 参数按请求宽高比例从图片高度 10% 处开始截取图片,然后缩放/放大到指定尺寸( 图片缩略图大小等于请求的宽高 )。

  • M 参数按请求宽高比例居中截图图片,然后缩放/放大到指定尺寸( 图片缩略图大小等于请求的宽高 )。

  • T 参数按请求宽高比例按比例缩放/放大到指定尺寸( 图片缩略图大小可能小于请求的宽高 )。

  • W 参数按请求宽高比例缩放/放大到指定尺寸,空白处填充白色背景颜色( 图片缩略图大小等于请求的宽高 )。

3.4.5 调用举例

正如前面所说,调用图片将采用如下所示格式:

http://oopul.vicp.net/12.jpg!c300x300.jpghttp://oopul.vicp.net/12.jpg!t300x300.jpghttp://oopul.vicp.net/12.jpg!m300x300.jpg

3.5 使用云服务进行压缩

提供图片服务的云平台有很多,这里以阿里云 OSS 为例。

3.5.1 关于存储

OSS 提供海量、安全、低成本、高可靠的云存储服务,提供 99.999999999% 的数据可靠性。使用 RESTful API 可以在互联网任何位置存储和访问,容量和处理能力弹性扩展,多种存储类型供选择全面优化存储成本。

3.5.2 关于缩略图

阿里云可以配置图片处理样式,在 OSS 后台 > 相应 Bucket > 图片处理 下新建样式如 thumb256 : enter image description here是要使用图片时,只需要按照如下格式即可使用。

域名/sample.jpg?x-oss-process=style/stylename
或者
域名/example.jpg@!panda_style

举个栗子:

http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=style/panda_style或者
http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg@!panda_style

3.6 关于图片旋转问题

在使用上传过程中,发现部分三星手机以及一部分苹果手机出现图片上传后旋转了 90 度的情况,解决思路如下:

Step1: 获得图片旋转角度 a,将 a 传递到后端,代码如下:

//引进Exif.js这个js能得到图片的一些旋转信息<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://cdn.jsdelivr.net/npm/exif-js"></script>
 <script> //定义回调函数,获得旋转角度。
 //orient是1-8之间的数字,1是正常的。
 //关于orient,可以看下图function getorient(base64data,callback){    var image = new Image();
    image.src = base64data;
    image.onload = function(){            var orient = getPhotoOrientation(image);
            callback(orient);
    }
}
 </scipt>

enter image description here

Step2:根据 a 参数,判断要旋转矫正的角度,然后进行旋转。

这里的难点在于寻找 orient 与旋转的对应关系,以及翻转后重新寻找中心点。对于 1、3、6、8 旋转可以做到。 对于 2、4、5、7 ,需要进行 X 轴或者 Y 轴翻转。

function getangle(orient){    switch (orient){        case 8:        return 2*Math.PI*270/360;        case 3:        return 2*Math.PI*180/360; 
        case 6:        return 2*Math.PI*90/360;case 1:        return 0;   
        default:        return 0;
    }
}

以 PHP 为例,src_im 为原图片,angle 为旋转的角度。

resource  imagerotate(resource src_im ,    float angle,    int bgd_color    [,int ignore_transpatrent])

Golang 图片旋转需要使用 Google 提供的包

import (
        "code.google.com/p/graphics-go/graphics"
   )
#核心代码
func main() {
     //加载图片
     src, err := LoadImage("src.png")
     if err != nil {
         log.Fatal(err)
     }
     //获得一个新的图片用来存放旋转后的图片
     dst := image.NewRGBA(image.Rect(0, 0, 350, 400))
     //下面这个就是旋转多少度
     err = graphics.Rotate(dst, src, &graphics.RotateOptions{3.5})
     if err != nil {
         log.Fatal(err)
     }
     // 需要保存的文件
     saveImage("dst.png", dst)
 }

运行效果如下,该例子来自互联网。

enter image description hereStep3:保存完成。

另外一种思路,在前端完成。需要用到 Canvas,用于前端 Canvas 数据采集质量较差,这里我们将不做介绍。

3.7 建议

就笔者经验,建议图片处理采用第三方云服务的形式,如七牛、阿里Oss、腾讯的 Oss 都是不错的选择。主要原因如下:

  1. 可以规避搭建图片服务器的一系列问题。

  2. 上传性能,下载响应速度都有保障。

  3. 存储空间够大,可以无限伸缩扩容。

  4. 提供的服务十分丰富,价钱合适。

后端接收图片

4.1 接收上传的文件

以 Golang 为例,后端上传文件后我们将文件一般按照如下流程进行处理:

func upload(w http.ResponseWriter, r *http.Request)string {
    //1.获取文件内容 要这样获取,这个file就是前端dom的名称
    srcfile, head, err := r.FormFile("file")

    //head.Filename=测试文件.jpg
    tmp := strings.Split(head.Filename,".");
    //2. 获得文件后缀  .jpg
    sufix := "."+tmp[len(tmp)-1]
    //3. 按照时间戳获得新文件名称 dstfilename
    dstfilename := fmt.Sprintf("%d",time.Now().Unix())
    //4. 创建新文件
    dstfile, err := os.Create(dstfilename  + sufix)

    //5. 最后将上传的带的图片copy,到目的文件处,进行处理
    _, err = io.Copy(dstfile, srcfile)
    //6. 返回文件路径
    return filename+sufix
}

具体起来,需要注意的是如下几个方面 :

  1. 解析上传文件操作函数 FormFile(filekey) 不能硬编码,在以上代码中已经被硬 编码成“file”,显然这是不利于扩展的,解决办法如下:

//javascript端添加一个字段,filekey;var file = this.files[0];var formdata=new FormData();var filekey = "file";
formdata.append("filekey",filekey);
formdata.append(filekey,file);
.....

后端接收时:

r.ParseForm()
//先解析filekey,获得filekey="file"
filekey := r.PostForm.Get("filekey")
//然后再根据key解析这个文件
srcfile, head, err := r.FormFile(filekey)
  1. 做好异常处理。文件上传过程比较耗费时间,并且受网络影响较大,因此会存在各种莫名其妙的错误,这里需要处理好。

  2. 封装友好的返回结果。文件上传结果提示要友好,这里以返回 Json 为例说明:

{"code":0,//code是错误码,0表示成功,其他错误码可以自定义"data":"url",//当上传成功后该字段内容为图片的url地址"msg":"",//上传过程发生错误时,该字段用作错误提示。
}

考虑到以上几个方面,我们给出如下例子。

//封装返回到前端的结果
func RespJson(w http.ResponseWriter,data interface{}){
    header :=w.Header()
    header.Set("Content-Type","application/json;charset=utf-8")
    w.WriteHeader(http.StatusOK)
    ret,err :=json.Marshal(data)
    if err!=nil{
        fmt.Println(err.Error())
    }
    w.Write(ret)
}
//定义返回的数据结构体
type H struct {
    Code int     `json:"code"`
    Data interface{} `json:"data,omitempty"`
    Msg string `json:"msg,omitempty"`
}
//具体上传列子
func upload(w http.ResponseWriter, r *http.Request){
    //获取文件内容 要这样获取,这个file就是前端dom的名称
    //r.ParseForm()
    //获得filekey=file
    filekey := r.PostFormValue("filekey")
    //解析这个字段对应的文件
    srcfile, head, err := r.FormFile(filekey)
    //错误一般是file名称不对导致的
    if err != nil {
        fmt.Println(err)
        RespJson(w,H{
            Code:-1,Msg:err.Error(),
        })
        return
    }
    defer func() {
        srcfile.Close()
    }()

    //创建文件,head.Filename=测试文件.jpg
    tmp := strings.Split(head.Filename,".");
    //获得文件后缀.jpg
    sufix := "."+tmp[len(tmp)-1]

    filename := fmt.Sprintf("%d",time.Now().Unix())

    //创建文件夹,用来存储文件
    os.MkdirAll("./mnt/",os.ModePerm)
    //然后打开一个文件
    dstfile, err := os.Create("./mnt/"+filename + sufix)
    defer dstfile.Close()
    if err != nil {
        RespJson(w,H{
            Code:-1,Msg:"文件保存失败",
        })
        return
    }
    //最后将上传的带的图片,进行处理
    _, err = io.Copy(dstfile, srcfile)
    defer func() {
        //上传上来的临时文件一律删除
        os.Remove(head.Filename)
    }()
    if err != nil {
        RespJson(w,H{
            Code:-1,Msg:"文件移动失败",
        })
        return
    }
    RespJson(w,H{
        Data: "/mnt/"+filename+sufix,
        Code:0,
    })
}

Curl 测试结果如下

>curl http://localhost:8080/upload -F "file=@./src.png" -F "filekey=file" -v
* Trying ::1...
* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> POST /upload HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Content-Length: 70703
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=------------------------11e46dfc83644466
>
< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=utf-8
< Date: Sat, 23 Feb 2019 13:47:37 GMT
< Content-Length: 39
<
{"code":0,"data":"/mnt/1550929657.png"}

注意其中的{"code":0,"data":"/mnt/1550929657.png"}是返回的Json

4.2 接收 Base64 字符串

4.2.1 前端编码注意事项

具体获得 Base64 编码我们已经做了详细的阐述,这里将不再描述。需要注意的是,Base64 格式编码包含一些特殊字符,如; / ? : @ & = + $ , #。 这些需要用 encodeURIComponent进行处理,否则,后端将只能获得获得如上所述任意特殊字符前的内容,举例如下:

//如前端通过字段base64data发送数据 data:image/bmp;base64,Qk12HhUAAA..//后端接收到的数据如下,从:号处被截断了data

此时后端返回 JSON

{"code":-1,"msg":"illegal base64 data at input byte 4"}

这是因为后端仅仅收到一个字符串 data ,因此不能识别为Base64 文件,所以报错。

4.2.2 后端编码注意事项

后端编码主要干俩个事情,一个是获得图片的格式类型,一个是将内容编码成文件。

  • 如何获得图片格式类型呢?这里提供俩种方法。

  1. 前端将格式类型传输到后端,后端接收。

//前端var fromdata = new FormData();
fromdata.append("filetype",".jpg")//后端接收filetype = r.PostFormValue("filetype")
  1. 后端根据类型自动解析

 //解析获得前端发过来的
 filekey := r.PostFormValue("filekey")//获得前端发过来的base64data:data:image/bmp;base64,Qk12HhUAAA..base64datawithhead:=r.PostFormValue(filekey) //将字符串截成俩部分
 base64data := strings.Split(base64datawithhead,";base64,")    if len(base64data)!=2{        return
    } //第二部分编码成文件内容
 filebuf,err := base64.StdEncoding.DecodeString(base64data[1])    //错误一般是file名称不对导致的
    if err != nil {
        fmt.Println(err)
        RespJson(w,H{
            Code:-1,Msg:err.Error(),
        })        return
    }    //默认一下文件类型为png,
    filetype := ".png"
    //如果包含data:image/png 就是png文件,其他依次类推
    if strings.Contains(base64data[0],"data:image/png"){
        filetype = ".png"
    }else if strings.Contains(base64data[0],"data:image/jpeg"){
        filetype = ".jpg"
    }else
    if strings.Contains(base64data[0],"data:image/gif"){
        filetype = ".gif"
    }else
    if strings.Contains(base64data[0],"data:image/bmp"){
        filetype = ".bmp"
    }else{
        RespJson(w,H{
            Code:-1,Msg:"不支持的文件格式",
        })        return
    }
  • 真正编码入图片的内容是 Base64 后面的内容。这意味着前端传入后端的数据需要在;base64, 处分割掉。

  • Java8 编码已经包含 Base64 编解码包,无需引入第三方包。Java7 需要引入第三方包。

  • 此方法可能存在 Post 传递参数字段超出大小限制的情况,需要对服务器进行配置。

Tomcat 配置如下。

#tomcat/conf/server.xml<Connector port="8080" protocol="HTTP/1.1"  
      connectionTimeout="20000"  
      redirectPort="8443" maxPostSize="0"/>maxPostSize="0" 表示取消大小限制

Nginx 配置如下:

#/etc/nginx/nginx.confserver{#location /{
    #配置最大1G
    client_max_body_size 1000m;
 }
}

Apache+PHP 类型服务器:

#httpd.conf中添加如下10*1024*1024=10485760LimitRequestBody 10485760#PHP.ini中添加#max_execution_time = 30 ,每个脚本运行的最长时间,单位秒,修改为:max_execution_time = 150#max_input_time = 60,每个脚本可以消耗的时间,单位也是秒,修改为:max_input_time = 300#memory_limit = 128M,脚本运行最大消耗的内存,根据你需求修改为:memory_limit = 256M#post_max_size = 8M,表单提交最大数据为 8M,此项不是限制上传单个文件的大小,而是针对整个表单的提交数据进行限制的。限制范围包括表单提交的所有内容.例如:发表贴子时,贴子标题,内容,附件等…这里修改为:post_max_size = 20M#upload_max_filesize = 2M ,上载文件的最大许可大小 ,修改为:upload_max_filesize = 10M

Golang 类 大小无限制,多么 Happy 的一件事啊!我爱Golang!

4.3 文件名称的学问

4.3.1 传统文件命名方法存在的问题

在日常开发中,很多人对图片文件命名无任何设计, 所以的到的图片大概是这样的:

https://www.imwinlion.com/wp-content/uploads/2016/04/新建图像-300×300.png

像这种图片结构,在应用根目录下新建一个 uploads 文件夹,然后下面分日期 2016/04/。这种方式,在小微型的应用中,还能勉强可用,一旦进入中大型应用场景,图片数量越来越多,并且,可能有其他的特殊需要,比如需要存储的是图片的 ID 时,我们该怎么做呢?这是很多初学者从来没想过的问题。

4.3.2 设计文件名称和存储路径

笔者研读阿里的 Fastdfs,并经过大量的应用实践,积累了一套实用性强,操作方便的图片命名策略,格式如下:

[hostID][depth][dirstr][filename][suffix]

这种名称策略怎么理解呢?

  • hostID:图片服务器资源 ID 这个涉及到我们自身的服务器资源规划问题。自定义的,比如我们应用占用了俩台服务器资源,一台是应用服务器,我们编号为 0,一台是资源服务器,我们将这个服务器资源编号为1,这个数据就是 hostID。

  • depth:相对于图片服务根目录来说,图片存储的文件夹目录深度,一般为俩层。比如我们存储的根目录为 /mnt/h5app/,那么这下面 的 目录a/b/c,depth 就是 3。

  • dirstr:目录字符串,如上,目录字符串就是 abc

  • filename:md5 策略或者uuID 策略生成 的文件名,如 md5(microtime() . mt_rand(1000,9999))

  • suffix:文件的后缀,如 .jpg、.png。

举个列子。假设我们的应用服务器服务器编号是 0,根目录是 /alIData/www/www.imwinlion.com/ ,对应的域名是 www.imwinlion.com。又假如我们的图片服务器有 3 台,一台编号是 1,图片存储目录是/mnt/www.imwinlion.com/ ,对应的域名是 res1.imwinlion.com;另一台编号是 2,图片存储目录是 /mnt/www.imwinlion.cn/,对应的域名是 res2.imwinlion.com

现在,我们根据我们设计的文件存储策略,假设名称是13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg,那么根据问件夹的第 1 位数字,我们很容易知道这个文件 hostID 是 1,文件存在编号为 1 的服务器上,也就是 /mnt/www.imwinlion.com/ 下。 根据第二个参数 depth 为 3,我们知道这个文件有 3 个父级子目录,那么接下来的三个字母 abc 就是 dirstr , 接下来的 01f9c76ce5c45aeec2e6f816c95b854b 是 Md5 生成的随机字符串。最后 .jpg 是文件格式,那么这个文件的存储路径应该是这样

/mnt/www.imwinlion.com/a/b/c/13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg

对应访问地址是这样

 http://res1.imwinlion.com/a/b/c/13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg

文件 ID 就是文件名称 13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg 。

我们数据库存这个 ID ,前端只要根据策略和这个ID就能够获得 图片路径。这种策略的核心在于先根据业务逻辑规划出图片存放路径。是先知道了文件路径,然后再存储。但是我们常用的框架比如 ThinkPHP,把文件的命名策略封闭起来了,我们只是先保存了文件,再获得一个返回的文件名,这是不可取的。

4.3.3 文件 ID 生成器

文件 ID 生成器,以 Golang 为例

//hostID:主机ID
//depth:深度ID
//dirstr:子目录字符串
//suffix:文件类型.png等
//return :文件路径(不包含hosID对于路径)和文件ID
func fileID(hostID,depth int,dirstr,suffix string)(path,ID string){
    var subdir []string = make([]string,0)
    for i:=0;i<depth;i++{
        subdir =append(subdir,fmt.Sprintf("%c",dirstr[rand.Intn(1024)%len(dirstr)]))
    }
    uuIDs,_ := uuID.NewV4()
    randomstr := base64.StdEncoding.EncodeToString([]byte(uuIDs.String()))
    ID =fmt.Sprintf("%d%d%s%s%s",hostID,depth,strings.Join(subdir,""),randomstr,suffix)
    path = fmt.Sprintf("%s/%s",strings.Join(subdir,"/"),ID)
    return
  }

提升图片服务的性能

图片存储服务在上传过程中占用大量的 IO 资源,在图片处理过程中占用大量的 CPU 资源,反应在用户端,就是很卡,很慢,我们要想办法提升图片应用服务性能。

5.1 支持异步

5.1.1 Java 类应用

一般部署在 Tomcat 容器上,Tomcat6 以后 protocol 支持 BIO 和 NIO。Nio 方式比 Bio 具有更好的并发性。同时,我们还可以扩大线程池数目 maxThreads。主要配置如下。

#tomcat 配置文件server.xml<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"maxThreads="150"connectionTimeout="20000"  redirectPort="8443" />
5.1.2 Golang 类应用

Golang 自身具备良好的并发性,Golang 可以采用携程机制,具体框架如下:

//定义最基础的处理单元type Task struct {
    Writer http.ResponseWriter
    Request *http.Request
}//定义任务管理器type TaskMgr struct {
    TaskQueue chan Task
}//初始化一个变量var taskMgr = TaskMgr{
    TaskQueue:make(chan Task,1024),
}//开启任务func StartTask(mgr TaskMgr){    go func() {        for{            select {                case task :=<-mgr.TaskQueue:                    go upload(task.Writer,task.Request)                    break;
            }
        }
    }()
}func  main()  {   //thumb("src.png","dst.png",930,500)
   //http.HandleFunc("/upload",upload)
   //将所有的/uploadbase64映射都发往chan队列中
   http.HandleFunc("/uploadbase64", func(writer http.ResponseWriter, request *http.Request) {        //需要对request对象进行特殊处理,要不然数据复制失败
        bodyBytes, _ := ioutil.ReadAll(request.Body)
        request.Body.Close()  //  must close
        request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))        //把数据传递到消息队列中
        taskMgr.TaskQueue<-Task{
        Writer:writer,
        Request:request,
       }
   })   //http.HandleFunc("/uploadbase64",uploadbase64)

   http.Handle("/html/",http.StripPrefix(       "/html/", http.FileServer(http.Dir("html"))))

    http.Handle("/mnt/",http.StripPrefix(        "/mnt/", http.FileServer(http.Dir("mnt"))))   //fmt.Println(fileID(1,2,"123456789abcedf",".png"))
   //开启消息chan服务
   StartTask(taskMgr)
   http.ListenAndServe(":8080",nil)
}

5.2 资源服务和应用服务分离

资源服务和应用服务不一样,资源服务在图片应用中主要表现在图片的读取和显示。而应用服务主要是对数据的读写,以及一些数据计算,业务类型是不一样的。因此针对这种情况,我们建议将资源与应用服务相互分离。

5.3 建立服务器级别缓存

一般来说,我们对资源类的服务做缓存支持,可以极大地提升响应速度,以一张 300kb 图片为例,不做缓存,响应速度 300ms,增加缓存配置后,响应速度降低到 80ms,这是非常有效的。 不同的服务器配置和网络环境上述参数是不一样的。

以 Nginx 为例, 参数举例如下:

proxy_cache          cachename;proxy_cache_valID      304 2h;proxy_cache_valID      403 444 24h;proxy_cache_valID      404 2h;proxy_cache_valID      500 502 2h;proxy_cache_use_stale    invalID_header http_403 http_404 http_500 http_502;proxy_cache_lock      on;proxy_cache_lock_timeout  5s;proxy_no_cache    $proxynocache_atomxml $proxynocache_sitemapxml;

•proxy_cache:对应 http 段的 keyzone,是你定义的 proxy_cache 所使用的共享空间的名称。 •proxy_cachevalID:对指定的 HTTP 状态进行缓存,并指定缓存时间。可以自定义写入多个配置项。 •proxy_cachestale:这个可以大大减少回源次数,因此可以将 inactive 适当延长。 •proxy_cachelock:同样是减少回源次数,和上面的差别在于缓存是否存在。 •proxy_no_cache: 0 表示使用缓存,其他任何值都表示不使用缓存。

另一方面,我们还可以使用 expires 指令对资源过期时间进行限制,以达到减少从服务器读取内容的次数的目的。

server {    listen       80 default_server;    server_name  www.imwinlion.com;    # 通过此语句来映射静态资源
    root         /data/www/html/;    #任何图片都缓存七天 
    location ~ .*\.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm)$
    {        expires      7d;
    }    #css 类的资源7天过期
    location ~ .*\.(?:js|css)$
    {        expires      7d;
    }    #html类的资源不缓存
    location ~ .*\.(?:htm|html)$
    {        add_header Cache-Control "private, no-store, no-cache, must-revalIDate, proxy-revalIDate";
    }
}

5.4 单机 Or 分布式文件系统

在系统开始我们犯了一个错误,搭建了分布式文件系统 Fastdfs,事实证明完全没必要。主要有如下几点:

  1. 小批量应用,完全可以由单机存储应付,因为硬盘存储价格便宜。

  2. 对于海量图片应用,建议采用 OSS。OSS 本身能提供海量资源存储功能,另外支持常用的图片服务,如裁剪、缩略图等,价格也适宜。

5.5 没有使用 CDN

CDN 加速似乎成为了海量资源应用服务的标配,但是我们这个应用没有使用 CDN 加速功能,因为甲方的客户都在南方。我们的服务器也在南方。重要的事情只说一遍:

用不用 CDN 应该视目标受众群的网络情况而定。

5.6 使用了子域名

为什么要使用子域名?因为子域名和应用域名分离,图片资源访问时,可以少携带一些参数,提高响应速度。

最初,我们文件系统部署在自建服务器上,随着文件越来越多,服务越来越慢。后来做了优化,使用了子域名,但是很尴尬,效果并不明显。

最后我们搬迁到 OSS 上,从此以后响应速度快了,存储空间不需担心了,图片裁剪性能也不用担心了,我们的程序兄弟过上了幸福的生活。

5.7 善于使用缩略图

前端列表显示时候,如果后端能提供缩略图功能,建议加上缩略图,这样做可以减小图片大小,减少图片下载时间,也就降低手机端图片的渲染时间。OSS 本身提供缩略图功能,大爱!

5.8 其他

其他的一些细节

  • 一页每次加载不超过 40 条记录,记录太多则加载时间长,页面卡顿。

  • 列表应用页面应支持上拉加载和下拉刷新,用户肯定不会多么讨厌你的应用。

  • 上传过程中一定要添加进度条提示,这样用户就不会焦躁不安啦。

  • 关键性操作按钮,比如上传按钮,应该加锁,防止用户重复点击。为什么?因为手机环境网络不稳定的所以上传时好时坏,用户要是觉得没有反应会再次点击,从此进入一个死循环。

获得源代码及更多支持

笔者已经将前端压缩代码重构成一个不到1kb的 JS,请加 betaidea  回复golang 获得。


点击查看更多内容
7人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消