移动端SeeApp开发总结-3

哟西

这次只讲一个东西:移动端图片压缩&上传

DOM

1
2
<input id="uploadPic_input" type="file" accept="image/*" style="display:block;opacity:0;" name="image">
<a id="uploadPic_btn" href="javascript:void(0);" class="a02">

在移动端,input大部分还是支持的,当类型为file时,苹果会弹出拍照/录像照片图库;手上没安卓= =,记得也是类似的选项。但是需求是只要图片,所以用了accpet这个属性,就能自动地把录像这个选项去掉了。
accept还支持选定的格式,语法:

1
<input accept="audio/*|video/*|image/*|MIME_type">

目前还是候选推荐标准(W3C Candidate Recommendation),但是支持的浏览器还是挺多的,测试中都能正常使用。
相关链接:accept
这里设置为透明是业务需要,默认不展示这个按钮。

上传流程

后台提供的上传图片的接口返回的是一个字符串,字符串的值是图片在服务器存放的路径。在整个流程的最后上传图片的路径,而不是图片。考虑到还要预览图片,就没有用FileReader这个接口来预览,而是等图片上传完毕后直接贴上链接,在上传图片时,还要有提示信息,比如上传进度。此外,预览的图片还能进行删除操作。在移动端,由于手机摄像头的升级,图片的体积一般很大,1~2M都是正常的范围,为了节省流量并加速,必须对图片进行压缩操作再进行上传,而服务器也会对图片进行压缩,所以最后再次请求的预览的图片其实很小。

图片压缩流程

参考文章:移动端图片上传后进行压缩功能

在移动端压缩图片并且上传主要用到filereader、canvas以及formdata这三个h5的api。逻辑并不难。整个过程就是:

  1. 用户使用input file上传图片的时候,用filereader读取用户上传的图片数据(base64格式)
  2. 把图片数据传入img对象,然后将img绘制到canvas上,再调用canvas.toDataURL对图片进行压缩
  3. 获取到压缩后的base64格式图片数据,转成二进制塞入formdata,再通过XmlHttpRequest提交formdata。

具体细节

利用FileReader获取图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var imageCompress = new (function (file) {
var filechooser = document.getElementById("uploadPic_input");
// 用于压缩图片的canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext('2d');
// 瓦片canvas
var tCanvas = document.createElement("canvas");
var tctx = tCanvas.getContext("2d");
var maxsize = 100 * 1024;
var progress_timer = null;
filechooser.onchange = function () {
if(window.navigator.userAgent.toLowerCase().indexOf('android') > -1) {
uploadPic.uploadWithnoCompress();
return;
}
if (!this.files.length) {
see.tips('请上传正确的图片');
return;
}
var files = Array.prototype.slice.call(this.files);
files.forEach(function (file, i) {
if (!/\/(?:jpeg|png|gif)/i.test(file.type)) {
see.tips('非法格式');
return;
}
var reader = new FileReader();
//获取图片大小
var size = file.size/1024 > 1024 ? (~~(10*file.size/1024/1024))/10 + "MB" : ~~(file.size/1024) + "KB";
reader.onload = function () {
var result = this.result;
var img = new Image();
img.src = result;
//如果图片大小小于100kb,则直接上传
if (result.length <= maxsize) {
img = null;
return;
}
//图片加载完毕之后进行压缩,然后上传
if (img.complete) {
callback();
} else {
img.onload = callback;
}
function callback() {
var data = compress(img);
upload(data, file.type, file.name);
img = null;
}
};
reader.readAsDataURL(file);
})
};
  • 由于很多微信内的安卓机并不支持Canvas的.toDataURL方法,这里安卓就用另一种没有压缩的办法来实现文件上传
  • 监听input元素,元素的files属性是一个FileList,存放着将要上传的文件,用Array.prototype.slice.call()方法将对象转换为数组,然后用forEach来遍历数组。
  • 紧接着对上传的文件进行一系列的判断,然后就开始调用FileReader接口了。
  • ~~是将浮点数转为整数,只保留小数部分
  • HTML5接口的FileReader有两个方法:readAsDataURLreadAsTextreadAsDataURL传入一个blob类型的数据,返回Data URL,一般是Base64的字符串。这个两个方法是异步的,因此,给reader定义一个onload事件,数据加载完成后,onload事件里的result就是处理完成后的结果。参考:FileReader
  • readAsDataURL完成后返回大概这么一个字符串:data:image/jpeg;base64,/9j/4AAQSkZJRgABA...QAASABIAAD/4QB=中间有7w多个字符,表示一张图片:

Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,大家可以查看RFC2045~RFC2049,上面有MIME的详细规范。Base64编码可用于在HTTP环境下传递较长的标识信息。例如,在Java Persistence系统Hibernate中,就采用了Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码为一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。

  • onload事件里,创建了一个Image对象,返回一个DOM元素,给Image对象赋DataURL的值。Image对象有很多属性,其中一个是complete,返回浏览器是否已完成对图像的加载。加载成功后,调用压缩方法,再调用上传的方法,否则等加载完成后再进行调用。参考:HTML DOM Image 对象 HTMLImageElement

用canvas压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function compress(img) {
var initSize = img.src.length;
var width = img.width;
var height = img.height;
//如果图片大于四百万像素,计算压缩比并将大小压至400万以下
var ratio;
if ((ratio = width * height / 4000000)>1) {
ratio = Math.sqrt(ratio);
width /= ratio;
height /= ratio;
}else {
ratio = 1;
}
canvas.width = width;
canvas.height = height;
//铺底色
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
//如果图片像素大于100万则使用瓦片绘制
var count;
if ((count = width * height / 1000000) > 1) {
count = ~~(Math.sqrt(count)+1); //计算要分成多少块瓦片`~~`是取整数部分
//计算每块瓦片的宽和高
var nw = ~~(width / count);
var nh = ~~(height / count);
tCanvas.width = nw;
tCanvas.height = nh;
for (var i = 0; i < count; i++) {
for (var j = 0; j < count; j++) {
tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh);
ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh);
}
}
} else {
ctx.drawImage(img, 0, 0, width, height);
}
//进行最小压缩
var ndata = canvas.toDataURL('image/jpeg', 0.1);
tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;
return ndata;
}
  • 压缩函数接受的参数是一个image类型的DOM节点,这个image的url是base64编码的,能直接读取数据
  • 压缩图片主要就是canvas的drawImage方法以及toDataURL方法
  • drawImage:接受八个参数,分别是:数据类型(DOM节点),起始x坐标,起始y坐标,图像宽,图像高(相对于图像而言),起始x坐标,起始y坐标,画布宽,画布高(相对于画布而言)。非常详细。主要功能是在画板上绘画图片。参考:drawImage
  • toDataURL:接受两个参数,数据类型和jpeg质量,返回一个字符串,就是base64编码的图片数据。参考:toDataURL
  • 下面是原文内容:

    在IOS中,canvas绘制图片是有两个限制的:
      首先是图片的大小,如果图片的大小超过两百万像素,图片也是无法绘制到canvas上的,调用drawImage的时候不会报错,但是你用toDataURL获取图片数据的时候获取到的是空的图片数据。
      再者就是canvas的大小有限制,如果canvas的大小大于大概五百万像素(即宽高乘积)的时候,不仅图片画不出来,其他什么东西也都是画不出来的。
      应对第一种限制,处理办法就是瓦片绘制了。瓦片绘制,也就是将图片分割成多块绘制到canvas上,我代码里的做法是把图片分割成100万像素一块的大小,再绘制到canvas上。
      而应对第二种限制,我的处理办法是对图片的宽高进行适当压缩,我代码里为了保险起见,设的上限是四百万像素,如果图片大于四百万像素就压缩到小于四百万像素。四百万像素的图片应该够了,算起来宽高都有2000X2000了。
      如此一来就解决了IOS上的两种限制了。
      除了上面所述的限制,还有两个坑,一个就是canvas的toDataURL是只能压缩jpg的,当用户上传的图片是png的话,就需要转成 jpg,也就是统一用canvas.toDataURL(“image/jpeg”, 0.1) , 类型统一设成jpeg,而压缩比就自己控制了。
      另一个就是如果是png转jpg,绘制到canvas上的时候,canvas存在透明区域的话,当转成jpg的时候透明区域会变成黑色,因为 canvas的透明像素默认为rgba(0,0,0,0),所以转成jpg就变成rgba(0,0,0,1)了,也就是透明背景会变成了黑色。解决办法就 是绘制之前在canvas上铺一层白色的底色。

上传图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function upload(basestr, type, filename) {
var text = window.atob(basestr.split(",")[1]);
var buffer = new Uint8Array(text.length);
var pecent = 0 , loop = null;
for (var i = 0; i < text.length; i++) {
buffer[i] = text.charCodeAt(i);
}
var blob = new Blob([buffer], {type: type});
var xhr = new XMLHttpRequest();
var formdata = new FormData();
//在一个Formdata里面传入多个数据,后台判断filename
formdata.append('image', blob, filename);
formdata.append('type','1');
xhr.open('post', 'http://m.seeapp.com/image/upload');
//设置与后台对应的请求头
xhr.setRequestHeader("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
xhr.setRequestHeader("Cache-Control","max-age=0");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
uploadPic.success($.parseJSON(xhr.responseText));
}
};
xhr.upload.addEventListener('progress', function (e) {
if (loop) return;
pecent = ~~(100 * e.loaded / e.total) / 2;
showProgress("正在上传中..." + pecent + "%");
if (pecent == 50) {
mockProgress();
}
}, false);
function mockProgress() {
if (loop) return;
loop = setInterval(function () {
pecent = pecent+2;
showProgress("正在上传中..." + pecent + "%");
if (pecent >= 99) {
clearInterval(loop);
}
}, 30)
}
xhr.send(formdata);
}
  • 首先是btoa()方法:

    由于一些网络通讯协议的限制,你必须使用 window.btoa() 方法对原数据进行编码后,才能进行发送。接收方使用相当于 window.atob() 的方法对接受到的 base64 数据进行解码,得到原数据。例如,发送某些含有 ASCII 码表中 0 到 31 之间的控制字符的数据。

  • 然后是Uint8Array对象:

The Uint8Array typed array represents an array of 8-bit unsigned integers. The contents are initialized to 0. Once established, you can reference elements in the array using the object’s methods, or using standard array index syntax (that is, using bracket notation).
Uint8Array数组类型表示一个8位无符号整型数组,创建时内容被初始化为0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。

  • 把base64的内容都转移到Uint8Array对象中
  • Blob对象

A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn’t necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user’s system.

  • 好吧这三个东西把我搞懵了=。=,都是理解意思,然而并不知道以后可以怎么用。
  • 在调试接口的时候,用文章原来的实现方法并不能成功,后来调试才发现需要一些必要的参数:formdata.append('image', blob, filename);。当然不同项目也不同,随机应变,查一下文档就能知道使用方法了。
  • 后面紧接着XMLHttpRequestFormData,这两个比较熟悉,就不贴了。另外用原生的xhr时,请求的头部一些必要的信息必须加上,通过测试用的接口和Chrome的工具来查看到底需要哪些参数,哪些头部。
  • 后面就是ajax请求了。新的接口提供了progress事件,可以检测上传的进度。

结束

  感觉写了好多= =。当时Sheen提出要压缩图片的时候,我内心是崩溃的..刚来公司几天就接手这个项目,好不容易摸清了ajax应用的套路,刚把无刷新上传写好就让我加个压缩功能~~不过后来百度了一下好像也没有特别难啦哈哈哈,虽然兼容性有点问题~后来在安卓版本的微信里面发现canvas有些功能不能用,就砍掉了,用一个上传插件来写,没有经过压缩的步骤。
  不过在ios端加上这个之后确实好了很多,很多图片在一般的网络环境上传也快了,在wifi下能看到正在上传50%然后一下子就好了也是挺爽的一件事。

分享到