移动端Web上传图片实践

前段时间项目上有一个拍照的需求,对于客户端当然是个小问题,但是PM要求该功能需要在网页版的页面上同样要实现跟客户端一样的体验!看到这个需求有点蒙,首先还不确定网页如何调用系统相机,选本地照片的话弄个<input type="file">应该就ok,其次手机拍一张照片都是几兆几兆的,如果不压缩一下图片,在这蛋疼的网络环境下,基本是没办法传到服务器的,网页上的环境也就那样,怎么做图片压缩呢?

1、上传方式

一般都是采用FormData提交

传统的<form enctype=”multipart/form-data” method=”post” action=”” target=”upload-form”> 配合 <iframe style=”display:none” name=”upload-form”></iframe>放到今天已经无法忍受了,好消息最新XHR2中支持把文件放在Formdata对象中异步提交,只考虑移动端,就可以舍弃iframe之类的兼容方案了。核心代码这样:

var xhr = new XMLHttpRequest();
var formData = new FormData();
formData.append(‘file‘, input.files[0]);
xhr.open(‘POST‘, form.action);
xhr.send(formData);

而且XHR2中还可以通过process事件来监听进度,实现类似进度条的功能,代码这样:

xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
    if (event.lengthComputable) {
        var percentComplete = event.loaded / event.total;
    ......
  }
}

用FormData发送的请求头中你的Content-Type 会变成这样 multipart/form-data; boundary=----WebKitFormBoundaryyqVkWF3PcCpAzZp9,如果上传时要附带参数也可以直接append到formData里。 

另外一种就是读取图片数据转成Base64编码或者二进制流提交,配合FormData使用提交

思路就是用JS把图片读到canvas中,然后用canvas.toDataURL()接口输出画布的base64编码,再把base64编码转成Blob塞到Formdata里传到后端。

这里贴一下twitter和webuploader的图片上传逻辑

send: function() {
                var owner = this.owner,
                    opts = this.options,
                    xhr = this._initAjax(),
                    blob = owner._blob,
                    server = opts.server,
                    formData, binary, fr;

                if ( opts.sendAsBinary ) {
                    server += (/\?/.test( server ) ? ‘&‘ : ‘?‘) +
                            $.param( owner._formData );

                    binary = blob.getSource();
                } else {
                    formData = new FormData();
                    $.each( owner._formData, function( k, v ) {
                        formData.append( k, v );
                    });

                    formData.append( opts.fileVal, blob.getSource(),
                            opts.filename || owner._formData.name || ‘‘ );
                }

                if ( opts.withCredentials && ‘withCredentials‘ in xhr ) {
                    xhr.open( opts.method, server, true );
                    xhr.withCredentials = true;
                } else {
                    xhr.open( opts.method, server );
                }

                this._setRequestHeader( xhr, opts.headers );

                if ( binary ) {
                    // 强制设置成 content-type 为文件流。
                    xhr.overrideMimeType &&
                            xhr.overrideMimeType(‘application/octet-stream‘);

                    // android直接发送blob会导致服务端接收到的是空文件。
                    // bug详情。
                    // https://code.google.com/p/android/issues/detail?id=39882
                    // 所以先用fileReader读取出来再通过arraybuffer的方式发送。
                    if ( Base.os.android ) {
                        fr = new FileReader();

                        fr.onload = function() {
                            xhr.send( this.result );
                            fr = fr.onload = null;
                        };

                        fr.readAsArrayBuffer( binary );
                    } else {
                        xhr.send( binary );
                    }
                } else {
                    xhr.send( formData );
                }
            }
// 压缩前的代码
...
convertCanvasToBlob:function(e){var t,i,s,n,r,a,o,c;for(n="image/jpeg",t=e.toDataURL(n),i=window.atob(t.split(",")[1]),r=new window.ArrayBuffer(i.length),a=new window.Uint8Array(r),s=0;s<i.length;s++)a[s]=i.charCodeAt(s);return o=window.WebKitBlobBuilder||window.MozBlobBuilder,o?(c=new o,c.append(r),c.getBlob(n)):new window.Blob([r],{type:n})}
...
function convertCanvasToBlob(canvas) {
    var format = "image/jpeg";
    var base64 = canvas.toDataURL(format);
    var code = window.atob(base64.split(",")[1]);
    var aBuffer = new window.ArrayBuffer(code.length);
    var uBuffer = new window.Uint8Array(aBuffer);
    for(var i = 0; i < code.length; i++){
        uBuffer[i] = code.charCodeAt(i);
    }
    var Builder = window.WebKitBlobBuilder || window.MozBlobBuilder;
    if(Builder){
        var builder = new Builder;
        builder.append(buffer);
        return builder.getBlob(format);
    } else {
        return new window.Blob([ buffer ], {type: format});
    }
}
这是它触屏版上传前的图片压缩逻辑之一,就是在前端把base64转成二级制数据,这个数据体积相比base64小很多,还可以塞到formdata中提交,不过不支持android 2及以下,ios 5.1及以下版本的浏览器。
我猜你的业务可能也是想实现类似这样的图片上传功能,分析twitter的源码可能会对你有一些帮助

2、读取图片

//绑定input change事件
$("#photo").unbind("change").on("change",function(){
    var file = this.files[0];
    if(file){
        //验证图片文件类型
        if(file.type && !/image/i.test(file.type)){
            return false;
        }
        var reader = new FileReader();
        reader.onload = function(e){
            //readAsDataURL后执行onload,进入图片压缩逻辑
            //e.target.result得到的就是图片文件的base64 string
            render(e.target.result);
        };
        //以dataurl的形式读取图片文件
        reader.readAsDataURL(file);
    }
});

3、前端图片压缩

图片上传的主体工作算是完成了,不过现在手机随便拍张照片就是一两兆,wifi环境下不说,移动网络通过这方案上传照片就有点坑了。手机客户端中一般会先压缩图片再上传,Web中如何实现压缩后上传呢?
可以把图片读到canvas中,然后用canvas.toDataURL()接口输出画布的base64编码,再把base64编码转成Blob塞到Formdata里传到后端。这样即可以压缩图片减少流量,又可以在前端就修正图片旋转的问题。当然这里面处理兼容的的坑很多,我们只说思路。

//定义照片的最大高度
var MAX_HEIGHT = 480;
var render = function(src){
    var image = new Image();
    image.onload = function(){
        var cvs = document.getElementById("cvs");
        var w = image.width;
        var h = image.height;
        //计算压缩后的图片长和宽
        if(h>MAX_HEIGHT){
            w *= MAX_HEIGHT/h;
            h = MAX_HEIGHT;
        }
        var ctx = cvs.getContext("2d");
        cvs.width = w;
        cvs.height = h;
        //将图片绘制到Canvas上,从原点0,0绘制到w,h
        ctx.drawImage(image,0,0,w,h);

        //进入图片上传逻辑
        sendImg();
    };
    image.src = src;
};

4、上传图片

var sendImg = function(){
    var cvs = document.getElementById("cvs");
    //调用Canvas的toDataURL接口,得到的是照片文件的base64编码string
    var data = cvs.toDataURL("image/jpeg");
    //base64 string过短显然就不是正常的图片数据了,过滤の。
    if(data.length<48){
        console.log("image data error.");
        return;
    }
    //图片的base64 string格式是data:/image/jpeg;base64,xxxxxxxxxxx
    //是以data:/image/jpeg;base64,开头的,我们在服务端写入图片数据的时候不需要这个头!
    //所以在这里只拿头后面的string
    //当然这一步可以在服务端做,但让闲着蛋疼的客户端帮着做一点吧~~~(稍微减轻一点服务器压力)
    data = data.split(",")[1];
    $.post("./api/uploadimg",{
        fileName:"xxx.jpeg",
        fileData:data
    },function(data){
        if(data.status==200){
            // some code here.
            console.log("commit image success.");
        }else{
            console.log("commit image failed.");
        }
    },"json");
};

看完上面的代码,是不是觉得也没那么难?真的是这样吗?code旅途艰辛,显然没那么容易就让你好过。

测试后发现,在pc上以及大部分android和iphone4s+上是正常的,但是极小部分android和iphone4s以下的机型上得到的照片居然是不完整的!比如只有上半部分,下半部分是黑的,或者照片是旋转的!开始以为是服务端图片存储的时候出了问题,不过后面排除了服务端的问题,看来上面代码是有兼容性问题的。

具体排除问题的过程很复杂纠结,就不细说了。贴几个帖子:

1.HTML5 Canvas drawImage ratio bug iOS

2.iOS HTML5 canvas drawImage vertical scaling bug, even for small images?

3.Drawing on canvas after megapix rendering is reversed

主要是低版本的ios safari上面对于大尺寸的照片(超过设备的物理像素)处理的bug,导致的现象就是上半部分是照片下半部分是黑的,我们需要一个工具将一张大图切成若干个小于屏幕尺寸的小图,分别对小图进行处理然后再合并成一张图片。原理很简单,但实现起来就没那么简单了,还是已经有相关的开源工具来完成这个工作。

Fixes iOS6 Safari‘s image file rendering issue for large size image (over mega-pixel), which causes unexpected subsampling when drawing it in canvas.

剩下一个图片旋转的问题,其实每张图片拍摄后EXIF里面都带有旋转Orientation字段来标注图片的旋转信息的,也就是说其实图片本身就是倒着的,但是图片展示的时候通过读取Orientation来修正图片展示,使图片能按照拍摄的角度展示,所以我们在写入图片数据的时候需要按照图片本身的Orientation来写入数据,这样我们就需要拿到图片本身的EXIF信息。

JavaScript library for reading EXIF image metadata

4、实际测试一下iOS没问题,Android 4 有些机型不行,貌似修改过file的Blob数据发到服务端的数据字节就会为0 这是安卓的bug https://code.google.com/p/android/issues/detail?id=39882 。 网上有人给出的解决方案是用FileReader把文件读出来,然后把整个二进制文件当请求发到服务端,这种方式要附带参数的话只能放url里了。

var reader = new FileReader();
reader.onload = function() {
    $.ajax({
                type: ‘POST‘,
                url: server,
                data: this.result,
                contentType: false,
                processData: false,
                beforeSend: function (xhr) {
                    xhr.overrideMimeType(‘application/octet-stream‘);
            },
            }).done(function (res) {
                ......
            }).fail(function () {
                ......
            }).always(function () {
                ......
            });
};
reader.readAsArrayBuffer(file);

ok,问题终于全部排除完毕啦。那么经过优化后的完整代码就是:

//绑定input change事件
$("#photo").unbind("change").on("change",function(){
    var file = this.files[0];
    if(file){
        //验证图片文件类型
        if(file.type && !/image/i.test(file.type)){
            return false;
        }
        var reader = new FileReader();
        reader.onload = function(e){
            //readAsDataURL后执行onload,进入图片压缩逻辑
            //e.target.result得到的就是图片文件的base64 string
            render(file,e.target.result);
        };
        //以dataurl的形式读取图片文件
        reader.readAsDataURL(file);
    }
});

//定义照片的最大高度
var MAX_HEIGHT = 480;
var render = function(file,src){
    EXIF.getData(file,function(){
        //获取照片本身的Orientation
        var orientation = EXIF.getTag(this, "Orientation");
        var image = new Image();
        image.onload = function(){
            var cvs = document.getElementById("cvs");
            var w = image.width;
            var h = image.height;
            //计算压缩后的图片长和宽
            if(h>MAX_HEIGHT){
                w *= MAX_HEIGHT/h;
                h = MAX_HEIGHT;
            }
            //使用MegaPixImage封装照片数据
            var mpImg = new MegaPixImage(file);
            //按照Orientation来写入图片数据,回调函数是上传图片到服务器
            mpImg.render(cvs, {maxWidth:w,maxHeight:h,orientation:orientation}, sendImg);
        };
        image.src = src;
    });
};

//上传图片到服务器
var sendImg = function(){
    var cvs = document.getElementById("cvs");
    //调用Canvas的toDataURL接口,得到的是照片文件的base64编码string
    var data = cvs.toDataURL("image/jpeg");
    //base64 string过短显然就不是正常的图片数据了,过滤の。
    if(data.length<48){
        console.log("data error.");
        return;
    }
    //图片的base64 string格式是data:/image/jpeg;base64,xxxxxxxxxxx
    //是以data:/image/jpeg;base64,开头的,我们在服务端写入图片数据的时候不需要这个头!
    //所以在这里只拿头后面的string
    //当然这一步可以在服务端做,但让闲着蛋疼的客户端帮着做一点吧~~~(稍微减轻一点服务器压力)
    data = data.split(",")[1];
    $.post("./api/uploadimg",{
        fileName:"xxx.jpeg",
        fileData:data
    },function(data){
        if(data.status==200){
            // some code here.
            console.log("commit image success.");
        }else{
            console.log("commit image failed.");
        }
    },"json");
};

实测一下,稍低端的的安卓上有点卡,毕竟处理一张图片的运算量可不小,目测目前用前端压缩上传方案的不多,至少微博触屏版 (http://m.weibo.cn/) 就是把原始图片直接上传的,这种方式是否适合直接使用或者还有哪些可以优化的地方有待验证。QQ空间触屏版图片上传是直接把图片base64编码发给服务端处理。

参考:

http://blog.lxjwlt.com/front-end/2015/05/22/canvas-deal-width-avatar.html

http://stackoverflow.com/questions/6974684/how-to-send-formdata-objects-with-ajax-requests-in-jquery

http://www.cnblogs.com/stephenykk/p/3558887.html

http://www.iunbug.com/archives/2012/06/04/208.html

http://www.iunbug.com/archives/2012/06/04/220.html

http://www.iunbug.com/archives/2012/06/05/234.html

http://www.iunbug.com/archives/2012/06/05/254.html

http://www.iunbug.com/archives/2012/06/06/273.html

http://iampeng.wang/javascript/2014/11/10/html5-fileapi-canvas-image.html

http://velocity.oreilly.com.cn/2013/ppts/web_app_performance_optimization_of_qzone_touch.pdf

https://www.imququ.com/post/how-to-auto-rotate-photo-in-js.html

http://madong.net.cn/index.php/2014/12/516/

https://github.com/xiangpaopao/blog/issues/7

http://gyf1.com/blog/2015/03/17/convert-data-uri-to-file/

http://www.html5rocks.com/zh/tutorials/file/xhr2/

https://developer.mozilla.org/en-US/docs/Web/API/FormData

https://developer.mozilla.org/en-US/docs/Web/API/Blob

http://www.zhihu.com/question/30692677

http://m.weibo.cn/

http://m.qzone.com/infocenter?g_f=#info/all#addMood=true

http://w.t.qq.com/touch

http://fex.baidu.com/webuploader/doc/index.html

时间: 2024-12-19 20:11:10

移动端Web上传图片实践的相关文章

移动端WEB前端开发最佳实践

移动端WEB前端开发最佳实践 Safari,Android Browser,Chrome都是以WebKit为核心的 移动设备和PC之间的页面现实存在差异(Safari中定义了viewport) 在移动设备和桌面端WEB前端开发中,事件绑定存在差异(移动触点) 页面控件设计存差异(点触不灵敏,虚拟键盘弹出框) 移动设备的网络带宽普遍比PC慢,移动页面要设置更简洁 PC页面兼容移动设备 使用流式布局 借助CSS Media queries(媒体查询)技术 使用合适的图片显示兼容方案 保持页面简洁,不

手机移动端WEB资源整合

meta基础知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /> 忽略将页面中的数字识别为电话号码 <meta name="format-detection" content=&

移动端web资源

meta基础知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /> 忽略将页面中的数字识别为电话号码 <meta name="format-detection" content=&

【超级干货】手机移动端WEB资源整合:转载

转载于:http://www.w3cfuns.com/notes/24611/fbba9cbd616e795360ea45515494aa53.html meta基础知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 忽略将页面中的数字识别为电话号码 忽略Android平台中对邮箱地址的识别 当网站添加到主屏幕快速启动方式,可隐藏地址栏,仅针对ios的safari 将网站添加到主屏幕快速启动方式,仅针对ios的safari顶端状态条的样式 viewport模板 viewport模板——通用

【超级干货】手机移动端WEB资源整合

meta基础知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /> 忽略将页面中的数字识别为电话号码 <meta name="format-detection" content=&

Redis的Python实践,以及四中常用应用场景详解——学习董伟明老师的《Python Web开发实践》

首先,简单介绍:Redis是一个基于内存的键值对存储系统,常用作数据库.缓存和消息代理. 支持:字符串,字典,列表,集合,有序集合,位图(bitmaps),地理位置,HyperLogLog等多种数据结构. 支持事务.分片.主从复之.支持RDB(内存数据保存的文件)和AOF(类似于MySQL的binlog)两种持久化方式.3.0加入订阅分发.Lua脚本.集群等特性. 命令参考:http://doc.redisfans.com 中文官网:http://www.redis.net.cn 安装(都大同小

《响应式Web设计实践》学习笔记

原书: 响应式Web设计实践 第2章 流动布局 1. 布局选项 传统的固定布局中存在很多问题, 随着屏幕大小的越来越多元化, 固定布局已经不能适用了. 在流动布局中, 度量的单位不再是像素, 而是变成了百分比. 弹性布局与流动布局类似, 但是通常情况下, 弹性布局中会以em来作为单位. 带来一个好处是随着用户增大或减小字体, 适用弹性布局的元素的宽度也会等比例地变化. 但是其也可能出现水平滚动条 混合布局 媒体查询: 媒体查询允许根据设备的信息----诸如屏幕宽度, 方向或者分辨率等属性来使用不

移动端web开发常见问题

上一篇总结了一些有关html5和css3的面试题,这一篇是有关于移动端web开发的常见问题,希望一样对你有一些帮助. Meta相关 1. 添加到主屏后的标题(IOS) <meta name="apple-mobile-web-app-title" content="标题"> 2. 启用 WebApp 全屏模式(IOS) 当网站添加到主屏幕后再点击进行启动时,可隐藏地址栏(从浏览器跳转或输入链接进入并没有此效果) <meta name="a

手机移动端web前端常见问题整理

移动端常见问题及解决方案 一.meta基础知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /> 忽略将页面中的数字识别为电话号码 <meta name="format-detection&