移动端利用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 相关文章推荐
jQuery下扩展插件和拓展函数的写法(匿名函数使用的典型例子)
Oct 20 Javascript
如何设置iframe高度自适应在跨域情况下的可用方法
Sep 06 Javascript
同域jQuery(跨)iframe操作DOM(实例讲解)
Dec 19 Javascript
javascript每日必学之基础入门
Feb 16 Javascript
浅析ES6的八进制与二进制整数字面量
Aug 30 Javascript
Bootstrap基本组件学习笔记之进度条(15)
Dec 08 Javascript
浅谈angular2的http请求返回结果的subcribe注意事项
Mar 01 Javascript
Vue的移动端多图上传插件vue-easy-uploader的示例代码
Nov 27 Javascript
angularJS开发注意事项
May 26 Javascript
使用node.js实现微信小程序实时聊天功能
Aug 13 Javascript
解决vue无法设置滚动位置的问题
Oct 07 Javascript
vue中父子组件传值,解决钩子函数mounted只运行一次的操作
Jul 27 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
程序员编程十条戒律
2009/07/09 PHP
php 图片加水印与上传图片加水印php类
2010/05/12 PHP
php中导出数据到excel时数字变为科学计数的解决方法
2013/02/03 PHP
php数组查找函数in_array()、array_search()、array_key_exists()使用实例
2014/04/29 PHP
jQuery+Ajax+PHP“喜欢”评级功能实现代码
2015/10/08 PHP
thinkPHP内置字符串截取函数用法详解
2016/11/15 PHP
thinkphp5.0自定义验证规则使用方法
2017/11/16 PHP
PHP For循环字母A-Z当超过26个字母时输出AA,AB,AC
2020/02/16 PHP
IE和firefox浏览器的event事件兼容性汇总
2009/12/06 Javascript
通过javascript的匿名函数来分析几段简单有趣的代码
2010/06/29 Javascript
Window.Open如何在同一个标签页打开
2014/06/20 Javascript
JS实现的另类手风琴效果网页内容切换代码
2015/09/08 Javascript
AngularJS模块详解及示例代码
2016/08/17 Javascript
vue.js初学入门教程(1)
2016/11/03 Javascript
CSS+jQuery实现简单的折叠菜单
2016/12/20 Javascript
微信小程序 数组(增,删,改,查)等操作实例详解
2017/01/05 Javascript
Cookies 和 Session的详解及区别
2017/04/21 Javascript
javascript实现考勤日历功能
2018/11/29 Javascript
node.js微信小程序配置消息推送的实现
2019/02/13 Javascript
this.$toast() 了解一下?
2019/04/18 Javascript
微信小程序使用GoEasy实现websocket实时通讯
2020/05/19 Javascript
微信小程序实现多张图片上传功能
2020/11/18 Javascript
用Python实现QQ游戏大家来找茬辅助工具
2014/09/14 Python
Python中datetime模块参考手册
2017/01/13 Python
Python实现的多项式拟合功能示例【基于matplotlib】
2018/05/15 Python
python退出命令是什么?详解python退出方法
2018/12/10 Python
详解python websocket获取实时数据的几种常见链接方式
2019/07/01 Python
python使用python-pptx删除ppt某页实例
2020/02/14 Python
python爬虫可以爬什么
2020/06/16 Python
Herschel美国官网:背包、手提袋及配件
2020/03/10 全球购物
高校生生产实习自我鉴定
2013/09/21 职场文书
设计顾问服务计划书
2014/05/04 职场文书
放飞梦想演讲稿600字
2014/08/26 职场文书
2014年协会工作总结
2014/11/22 职场文书
诚信考试主题班会
2015/08/17 职场文书
HTML+CSS制作心跳特效的实现
2021/05/26 HTML / CSS