移动端利用H5实现压缩图片上传功能


Posted in Javascript onMarch 29, 2017

此前有同事跟我聊过关于移动端用canvas压缩图片后再上传的功能,最近有了点空闲时间,所以就实践了一下。demo效果链接在文章底部贴出。

在做移动端图片上传的时候,用户传的都是手机本地图片,而本地图片一般都相对比较大,拿iphone6来说,平时拍很多图片都是一两M的,如果直接这样上传,那图片就太大了,如果用户用的是移动流量,完全把图片上传显然不是一个好办法。

目前来说,HTML5的各种新API都在移动端的webkit上得到了较好的 实现。根据查看caniuse,本demo里使用到的FileReader、Blob、Formdata对象均已在大部分移动设备浏览器中得到了实现 (safari6.0+、android 3.0+),所以直接在前端压缩图片,已经成了很多移动端图片上传的必备功能了。

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

(1)用户使用input file上传图片的时候,用filereader读取用户上传的图片数据(base64格式)

(2)把图片数据传入img对象,然后将img绘制到canvas上,再调用canvas.toDataURL对图片进行压缩

(3)获取到压缩后的base64格式图片数据,转成二进制塞入formdata,再通过XmlHttpRequest提交formdata。

如此三步,就完成了图片的压缩和上传。

说起来好像挺简单,其实还是有些坑的。接下来就直接用代码进行分析:

【一】获取图片数据

先是获取图片数据,也就是监听input file的change事件,然后获取到上传的文件对象files,将类数组的files转成数组,然后进行forEach遍历。

接着判断文件类型,如果不是图片则不作处理。如果是图片就实例化一个filereader,以base64格式读取上传的文件数据,判断数据长度,如果大于200KB的图片就调用compress方法进行压缩,否则调用upload方法进行上传。

filechooser.onchange = function () {
   if (!this.files.length) return;
 
   var files = Array.prototype.slice.call(this.files);
 
   if (files.length > 9) {
    alert("最多同时只可上传9张图片");
    return;
   }
 
   files.forEach(function (file, i) {
    if (!/\/(?:jpegpnggif)/i.test(file.type)) return;
 
    var reader = new FileReader();
 
    var li = document.createElement("li");
    li.innerHTML = "<div class="progress"><span></span></div>";
    $(".img-list").append($(li));
 
    reader.onload = function () {
     var result = this.result;
     var img = new Image();
     img.src = result;
 
     //如果图片大小小于200kb,则直接上传
     if (result.length <= maxsize) {
      $(li).css("background-image", "url(" + result + ")");
      img = null;
      upload(result, file.type, $(li));
 
      return;
     }
 
 //    图片加载完毕之后进行压缩,然后上传
     if (img.complete) {
      callback();
     } else {
      img.onload = callback;
     }
 
     function callback() {
      var data = compress(img);
 
      $(li).css("background-image", "url(" + data + ")");
 
      upload(data, file.type, $(li));
 
      img = null;
     }
 
    };
 
    reader.readAsDataURL(file);
   })
  };

【2】压缩图片

上面做完图片数据的获取后,就可以做compress压缩图片的方法了。而压缩图片也并不是直接把图片绘制到canvas再调用一下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上铺一层白色的底色。

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);

  console.log("压缩前:" + initSize);
  console.log("压缩后:" + ndata.length);
  console.log("压缩率:" + ~~(100 * (initSize - ndata.length) / initSize) + "%");

  tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;

  return ndata;
 }

【三】图片上传

完成图片压缩后,就可以塞进formdata里进行上传了,先将base64数据转成字符串,再实例化一个ArrayBuffer,然后将字符 串以8位整型的格式传入ArrayBuffer,再通过BlobBuilder或者Blob对象,将8位整型的ArrayBuffer转成二进制对象 blob,然后把blob对象append到formdata里,再通过ajax发送给后台即可。

XmlHttpRequest2中不仅可以发送大数据,还多出了比如获取发送进度的API,我代码里也进行了简单的实现。

// 图片上传,将base64的图片转成二进制对象,塞进formdata上传
 function upload(basestr, type, $li) {
  var text = window.atob(basestr.split(",")[1]);
  var buffer = new ArrayBuffer(text.length);
  var ubuffer = new Uint8Array(buffer);
  var pecent = 0 , loop = null;

  for (var i = 0; i < text.length; i++) {
   ubuffer[i] = text.charCodeAt(i);
  }

  var Builder = window.WebKitBlobBuilder window.MozBlobBuilder;
  var blob;

  if (Builder) {
   var builder = new Builder();
   builder.append(buffer);
   blob = builder.getBlob(type);
  } else {
   blob = new window.Blob([buffer], {type: type});
  }

  var xhr = new XMLHttpRequest();
  var formdata = new FormData();
  formdata.append("imagefile", blob);

  xhr.open("post", "/cupload");

  xhr.onreadystatechange = function () {
   if (xhr.readyState == 4 && xhr.status == 200) {
    console.log("上传成功:" + xhr.responseText);

    clearInterval(loop);

    //当收到该消息时上传完毕
    $li.find(".progress span").animate({"width": "100%"}, pecent < 95 ? 200 : 0, function () {
     $(this).html("上传成功");
    });

    $(".pic-list").append("<a href="" + xhr.responseText + " rel="external nofollow" ">" + xhr.responseText + "<img src="" + xhr.responseText + "" /></a>")
   }
  };

  //数据发送进度,前50%展示该进度
  xhr.upload.addEventListener("progress", function (e) {
   if (loop) return;

   pecent = ~~(100 * e.loaded / e.total) / 2;
   $li.find(".progress span").css("width", pecent + "%");

   if (pecent == 50) {
    mockProgress();
   }
  }, false);

  //数据后50%用模拟进度
  function mockProgress() {
   if (loop) return;

   loop = setInterval(function () {
    pecent++;
    $li.find(".progress span").css("width", pecent + "%");

    if (pecent == 99) {
     clearInterval(loop);
    }
   }, 100)
  }

  xhr.send(formdata);
 }

     至此,整个上传的前端图片压缩就完成了,因为是用了formdata提交,所以后台接数据的时候就跟普通form表单提交数据一样处理即可。

如果对该demo有兴趣的可以看这个demo的github地址:

前端代码:https://github.com/whxaxes/node-test/blob/master/server/upload/index_2.html

顺便也贴出后台的实现(nodejs):https://github.com/whxaxes/node-test/blob/master/server/upload/upload_2.js  

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript重复绑定事件造成的后果说明
Mar 02 Javascript
Node.js node-schedule定时任务隔多少分钟执行一次的方法
Feb 10 Javascript
jQuery聚合函数实例
May 21 Javascript
jquery实现的树形目录实例
Jun 26 Javascript
跟我学习javascript解决异步编程异常方案
Nov 23 Javascript
常见JS验证脚本汇总
Dec 01 Javascript
JS获取和修改元素样式的实例代码
Aug 06 Javascript
bootstrap select插件封装成Vue2.0组件
Apr 17 Javascript
JavaScript算法教程之sku(库存量单位)详解
Jun 29 Javascript
javascript设计模式 ? 简单工厂模式原理与应用实例分析
Apr 09 Javascript
Vue3.0的优化总结
Oct 16 Javascript
vant-ui组件调用Dialog弹窗异步关闭操作
Nov 04 Javascript
浅析JS中的 map, filter, some, every, forEach, for in, for of 用法总结
Mar 29 #Javascript
Vue.js展示AJAX数据简单示例讲解
Mar 29 #Javascript
JS实现侧边栏鼠标经过弹出框+缓冲效果
Mar 29 #Javascript
Mongoose经常返回e11000 error的原因分析
Mar 29 #Javascript
js实现一个简单的数字时钟效果
Mar 29 #Javascript
jquery实现tab键进行选择后enter键触发click行为
Mar 29 #jQuery
Vue2组件tree实现无限级树形菜单
Mar 29 #Javascript
You might like
PHP框架Laravel学习心得体会
2015/10/28 PHP
PHP获取文件扩展名的常用方法小结【五种方式】
2018/04/27 PHP
js实现的跟随鼠标移动的时钟效果(中英文日期显示)
2011/01/17 Javascript
JavaScript中的私有/静态属性介绍
2012/07/26 Javascript
JavaScript表达式:URL 协议介绍
2013/03/10 Javascript
Javascript WebSocket使用实例介绍(简明入门教程)
2014/04/16 Javascript
node.js中的path.isAbsolute方法使用说明
2014/12/08 Javascript
JS去除iframe滚动条的方法
2015/04/01 Javascript
JS中多步骤多分步的StepJump组件实例详解
2016/04/01 Javascript
JavaScript中的Reflect对象详解(ES6新特性)
2016/07/22 Javascript
深入理解jQuery3.0的domManip函数
2016/09/01 Javascript
详解webpack进阶之loader篇
2017/08/23 Javascript
vue实现图片加载完成前的loading组件方法
2018/02/05 Javascript
在小程序中使用Echart图表的示例代码
2018/08/02 Javascript
浅谈Webpack核心模块tapable解析
2018/09/11 Javascript
Vuex 使用 v-model 配合 state的方法
2018/11/13 Javascript
详解如何实现Element树形控件Tree在懒加载模式下的动态更新
2019/04/25 Javascript
微信小程序实现点击空白隐藏的方法示例
2019/08/13 Javascript
vuex + keep-alive实现tab标签页面缓存功能
2019/10/17 Javascript
[25:45]2018DOTA2亚洲邀请赛4.5SOLO赛 Sylar vs Paparazi
2018/04/06 DOTA
[01:00:14]DOTA2-DPC中国联赛 正赛 Ehome vs Elephant BO3 第二场 2月28日
2021/03/11 DOTA
python基础教程之数字处理(math)模块详解
2014/03/25 Python
Python字符串格式化
2015/06/15 Python
Python实现SMTP发送邮件详细教程
2021/03/02 Python
python检测主机的连通性并记录到文件的实例
2018/06/21 Python
PyQt中使用QtSql连接MySql数据库的方法
2020/07/28 Python
python时间time模块处理大全
2020/10/25 Python
python 实现百度网盘非会员上传超过500个文件的方法
2021/01/07 Python
Oakley官网:运动太阳镜、雪镜和服装
2016/09/30 全球购物
复古斯堪的纳维亚儿童服装:Baby go Retro
2017/09/09 全球购物
应届毕业生求职自荐书
2014/01/03 职场文书
岗位职责的构建方法
2014/02/01 职场文书
档案室主任岗位职责
2014/02/12 职场文书
2014年市场部工作总结
2014/11/25 职场文书
校园安全学习心得体会
2016/01/18 职场文书
关于应聘教师的自荐信
2016/01/28 职场文书