HTML5 移动端图片上传的实践

主要的思路是这样:

监听一个 input (type='file') 的 change 事件,然后拿到文件的 file;
把 file 转成 dataURL;
然后用 canvas 绘制图片,绘制的时候经过算法按比例裁剪;
然后再把 canvas 转成 dataURL;
再把 dataURL 转成 blob;
接着把 blob append 到 FormData 的实例对象。
最后上传。
主要用到的 FileReader、canvas、FormData、Blob 这几个 API。

开发过程遇到了蛮多坑,特别是在android下的微信浏览器内。

监听 input(type=file) 获取文件内容。

// html 片段

<input type="file" id="file-input" name="image" accept="image/gif, image/jpeg, image/png">
对于 type 为 file 的 input 我们可以设置 accept 属性来现在我们要上传的文件类型,这里的目的是上传图片文件,所以我们可以设置:accept="image/gif, image/jpeg, image/png"。

// JavaScript
document.getElementById('file-input').onchange= function (event) {
  // 通过 event.target 回去 input 元素对象,然后拿到 files list,取第一个 file
  let file = event.target.files[0];
  // compressImage 在下面解释,它接受三个参数,文件、裁剪的长宽比例,回调函数(回调函数获得一个 FormData 对象,文件已经存在里面了);
  compressImage(file, [1, 1], (targetFormData) => {
    //...... 这里获取到了 targetFormData,就可以直接使用它上传了
  });
};
fileToDataURL: file 转成 dataURL

这里用到的是 FileReader 这个 API。
https://developer.mozilla.org/en-US/docs/Web/API/FileReader

/**
 * file 转成 dataURL
 * @param file 文件
 * @param callback 回调函数
 */
function fileToDataURL (file, callback) {
  const reader = new window.FileReader();
  reader.onload = function (e) {
    callback(e.target.result);
  };
  reader.readAsDataURL(file);
}
compressDataURL:dataURL 图片绘制 canvas,然后经过处理(裁剪 & 压缩)再转成 dataURL

一开始是这样的

我们需要创建一个 Image 对象,然后把 src 设置成 dataURL ,获取到这张图片;
我们需要创建一个 canvas 元素,用来处理绘制图片;
获取裁剪的长宽比例,然后判断图片的实际长宽比例,按照最大化偏小的长或宽然后另一边采取中间部分,和 css 把 background 设置 center / cover 一个道理;
调用ctx.drawImage绘制图片;
使用 canvas.toDataURL 把 canvans 转成 dataURL。
/**
 * 使用 canvas 压缩处理 dataURL
 * @param dataURL
 * @param ratio 比例
 * @param callback
 */
function compressDataURL (dataURL, ratio, callback) {
  // 1
  const img = new window.Image();
  img.src = dataURL;
  // 2
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  // 3
  canvas.width = 100 * ratio[0];
  canvas.height = 100 * ratio[2];
  const RATIO = canvas.width / canvas.height;
  let cutx = 0;
  let cuty = 0;
  let cutw = img.width;
  let cuth = img.height;
  if (cutw / cuth > RATIO) {
    // 宽超过比例了]]
    let realw = cuth * RATIO;
    cutx = (cutw - realw) / 2;
    cutw = realw;
  } else if (cutw / cuth < RATIO) {
    // 长超过比例了]]
    let realh = cutw / RATIO;
    cuty = (cuth - realh) / 2;
    cuth = realh;
  }
  // 4
  ctx.drawImage(img, cutx, cuty, cutw, cuth, 0, 0, canvas.width, canvas.height);
  const ndata = canvas.toDataURL('image/jpeg', 1);
  callback(ndata);
}
一切的运行在pc端的chrome浏览器下模拟都很好,但是在移动端测试的时候发现 canvas 无法绘制出图片,发现是 img 设置 src 有延迟,导致还没获取到图片图像就开始绘制。
改进:监听 img.onload 事件来处理之后的操作:

/**
 * 使用 canvas 压缩 dataURL
 * @param dataURL
 * @param ratio
 * @param callback
 */
function compressDataURL (dataURL, ratio, callback) {
  const img = new window.Image();
  img.src = dataURL;
  // onload
  img.onload = function () {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = 100 * ratio.width;
    canvas.height = 100 * ratio.height;
    const RATIO = canvas.width / canvas.height;
    let cutx = 0;
    let cuty = 0;
    let cutw = img.width;
    let cuth = img.height;
    if (cutw / cuth > RATIO) {
      // 宽超过比例了]]
      let realw = cuth * RATIO;
      cutx = (cutw - realw) / 2;
      cutw = realw;
    } else if (cutw / cuth < RATIO) {
      // 长超过比例了]]
      let realh = cutw / RATIO;
      cuty = (cuth - realh) / 2;
      cuth = realh;
    }
    ctx.drawImage(img, cutx, cuty, cutw, cuth, 0, 0, canvas.width, canvas.height);
    const ndata = canvas.toDataURL('image/jpeg', 1);
    callback(ndata);
  };
}
dataURLtoBlob:dataURL 转成 Blob

这一步我们把 dataURL 转成 Blob

/**
 * dataURL 转成 blob
 * @param dataURL
 * @return blob
 */
function dataURLtoBlob (dataURL) {
  let binaryString = atob(dataURL.split(',')[1]);
  let arrayBuffer = new ArrayBuffer(binaryString.length);
  let intArray = new Uint8Array(arrayBuffer);
  let mime = dataURL.split(',')[0].match(/:(.*?);/)[1]

  for (let i = 0, j = binaryString.length; i < j; i++) {
    intArray[i] = binaryString.charCodeAt(i);
  }

  let data = [intArray];

  let result = new Blob(data, { type: mime });
  return result;
}
很完美了吗,在pc端模拟成功,在移动端chrome浏览器测试成功,但是在微信浏览器中失败,经过 try...catch 发现是在 new Blob 的时候失败。
查看之后发现是这个 API 对 Android 的支持还不明。
解决方法是利用 BlobBuilder 这个老 API 来解决:https://developer.mozilla.org/en-US/docs/Web/API/BlobBuilder
因为这个 API 已经被遗弃,不同机型和安卓版本兼容性不一致,所以需要一个判断。
解决方法:

/**
 * dataURL 转成 blob
 * @param dataURL
 * @return blob
 */
function dataURLtoBlob (dataURL) {
  let binaryString = atob(dataURL.split(',')[1]);
  let arrayBuffer = new ArrayBuffer(binaryString.length);
  let intArray = new Uint8Array(arrayBuffer);
  let mime = dataURL.split(',')[0].match(/:(.*?);/)[1]

  for (let i = 0, j = binaryString.length; i < j; i++) {
    intArray[i] = binaryString.charCodeAt(i);
  }

  let data = [intArray];

  let result;

  try {
    result = new Blob(data, { type: mime });
  } catch (error) {
    window.BlobBuilder = window.BlobBuilder ||
      window.WebKitBlobBuilder ||
      window.MozBlobBuilder ||
      window.MSBlobBuilder;
    if (error.name === 'TypeError' && window.BlobBuilder){
      var builder = new BlobBuilder();
      builder.append(arrayBuffer);
      result = builder.getBlob(type);
    } else {
      throw new Error('没救了');
    }
  }

  return result;
}
把获取到的 blob append 到 FormData 实例,执行回调

这一步使用到我们之前的东西。

/**
 * 压缩图片
 * @param file 图片文件
 * @param ratio 比例
 * @param callback 回调,得到一个 包含文件的 FormData 实例
 */
function compressImage (file, ratio, callback) {
  fileToDataURL(file, (dataURL) => {
    compressDataURL(dataURL, ratio, (newDataURL) => {
      const newBlob = dataURLtoBlob(newDataURL);

      const oData = new FormData();
      oData.append('file', blob);

      callback(oData);
    });
  });
}
回到第一步,上传文件

// JavaScript
document.getElementById('file-input').onchange= function (event) {
  // 通过 event.target 回去 input 元素对象,然后拿到 files list,取第一个 file
  let file = event.target.files[0];
  // 接受三个参数,文件、裁剪的长宽比例,回调函数(回调函数获得一个 FormData 对象,文件已经存在里面了);
  compressImage(file, [1, 1], (targetFormData) => {

    let xhr = new XMLHttpRequest();

    // 进度监听
    // xhr.upload.addEventListener('progress', progFoo, false);
    // 加载监听
    // xhr.addEventListener('load', loadFoo, false);
    // 错误监听
    // xhr.addEventListener('error', errorFoo, false);

    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          // 上传成功,获取到结果 results
          let results = JSON.parse(xhr.responseText);
          // ......
          }
        } else {
          // 上传失败
        }
      }
    };
    xhr.open('POST', '/api/upload', true);
    xhr.send(targetFormData);
  });
};
一切似乎都很完美,pc 端模拟测试通过,但是到移动端却发现上传了一个空文件,这不科学!!!
查文档后发现这么一句话:

Note: XHR in Android 4.0 sends empty content for FormData with blob.
简直蒙蔽。
在 上找到了解决方案:http://stackoverflow.com/questions/15639070/empty-files-uploaded-in-android-native-browser/28809955#28809955

通过自己包装 FormDataShim 和重写 XMLHttpRequest.prototype.send 函数:

// Android上的AppleWebKit 534以前的内核存在一个Bug,
// 导致FormData加入一个Blob对象后,上传的文件是0字节
// QQ X5浏览器也有这个BUG
var needsFormDataShim = (function () {
  var bCheck = ~navigator.userAgent.indexOf('Android') &&
               ~navigator.vendor.indexOf('Google') &&
              !~navigator.userAgent.indexOf('Chrome');

  return bCheck && navigator.userAgent.match(/AppleWebKit\/(\d+)/).pop() <= 534 || /MQQBrowser/g.test(navigator.userAgent);
})();

// 重写 Blob 构造函数,在 XMLHttpRequest.prototype.send 中会使用到
var BlobConstructor = ((function () {
  try {
    new Blob();
    return true;
  } catch (e) {
    return false;
  }
})()) ? window.Blob : function (parts, opts) {
  let bb = new (
    window.BlobBuilder ||
    window.WebKitBlobBuilder ||
    window.MSBlobBuilder ||
    window.MozBlobBuilder
  );
  parts.forEach(function (p) {
    bb.append(p);
  });
  return bb.getBlob(opts ? opts.type : undefined);
};

// 手动包装 FormData 同时重写 XMLHttpRequest.prototype.send
var FormDataShim = (function () {
  var formDataShimNums = 0;
  return function FormDataShim () {
    var o = this;

    // Data to be sent
    let parts = [];

    // Boundary parameter for separating the multipart values
    let boundary = Array(21).join('-') + (+new Date() * (1e16 * Math.random())).toString(36);

    // Store the current XHR send method so we can safely override it
    let oldSend = XMLHttpRequest.prototype.send;
    this.getParts = function () {
      return parts.toString();
    };
    this.append = function (name, value, filename) {
      parts.push('--' + boundary + '\r\nContent-Disposition: form-data; name="' + name + '"');

      if (value instanceof Blob) {
        parts.push('; filename="' + (filename || 'blob') + '"\r\nContent-Type: ' + value.type + '\r\n\r\n');
        parts.push(value);
      } else {
        parts.push('\r\n\r\n' + value);
      }
      parts.push('\r\n');
    };

    formDataShimNums++;
    XMLHttpRequest.prototype.send = function (val) {
      let fr;
      let data;
      let oXHR = this;
      if (val === o) {
        // Append the final boundary string
        parts.push('--' + boundary + '--\r\n');
        // Create the blob
        data = new BlobConstructor(parts);

        // Set up and read the blob into an array to be sent
        fr = new FileReader();
        fr.onload  = function () {
          oldSend.call(oXHR, fr.result);
        };
        fr.onerror = function (err) {
          throw err;
        };
        fr.readAsArrayBuffer(data);

        // Set the multipart content type and boudary
        this.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
        formDataShimNums--;
        if (formDataShimNums === 0) {
          XMLHttpRequest.prototype.send = oldSend;
        }
      } else {
        oldSend.call(this, val);
      }
    };
  };
})();
SUCCESS

重写 compressImage

/**
 * 压缩图片
 * @param file 图片文件
 * @param ratio 比例
 * @param callback 回调,得到一个 包含文件的 FormData 实例
 */
function compressImage (file, ratio, callback) {
  fileToDataURL(file, (dataURL) => {
    compressDataURL(dataURL, ratio, (newDataURL) => {
      const newBlob = dataURLtoBlob(newDataURL);

      // 判断是否需要我们之前的重写
      let NFormData = needsFormDataShim() ? FormDataShim : window.FormData;

      const oData = new NFormData();
      oData.append('file', blob);

      callback(oData);
    });
  });
}
到这一步总算成功。

时间: 2024-09-26 05:10:44

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

jQuery移动端图片上传组件_jquery

本文实例为大家分享了移动端图片上传组件,供大家参考,具体内容如下 Imageupload使用File API+canvas 客户端压缩图片,并实现文件上传服务端 文件依赖 JQUERY 参数API loading:'.loading', 页面显示loading的图标selector url:'', 接收数据的api接口地址 maxFileSize:1010241024, 服务端支持的最大单文件大小 format:/^image/i, 支持的文件格式. images text ..... isCo

移动端html5图片上传方法【更好的兼容安卓IOS和微信】_Android

之前的移动端上传的方法,有些朋友测试说微信支持不是很好,还有部分安卓机也不支持,其实我已经有了另一个方法,但是例子还没整理出来,而联系我的很多朋友需要,所以就提前先发出来了,并且做一个简单的说明,就不做一个demo了. <!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=dev

项目实践一图片上传之form表单还是base64前端图片压缩(前端图片压缩)_javascript技巧

第一个项目终于上线了,是一个叫亲青筹的公益众筹平台,微信端,电脑端还有后台界面大部分都是我完成的,几个月过来,感觉收获了很多,觉得要总结一下. 首先想到的是图片上传的问题.在通常表单数据都是ajax上传的情况下,为了上传图片而去使用form表单感觉很蠢.然后那时候也没有想到用jquery form插件. 后台的同事给的方案是用iframe里写一个form表单,然后上传图片之后自动提交表单,他将图片在服务器上的地址以跳转页url的一部分,我再来截取的方式. 方案一:iframe+form表单 <f

html5在小米手机上图片上传无法获取文件名

问题描述 html5在小米手机上图片上传无法获取文件名 <input type=""file"" id=""file1"" name=""file1"" onchange=""preImage(1this.files);""accept=""image/*"">function preImage(

php+html5实现无刷新图片上传教程_php实例

本篇向大家介绍一种全新的上传图片的方式,利用html5的FileReader读取图片文件,然后将数据传输到服务器再使用PHP进行处理.实现过程如下(带图片预览功能) 前端html代码 upload,html <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content=&qu

html5 图片上传,支持图片预览、压缩、及进度显示,兼容IE6+及标准浏览器

原文:html5 图片上传,支持图片预览.压缩.及进度显示,兼容IE6+及标准浏览器 以前写过上传组件,见 打造 html5 文件上传组件,实现进度显示及拖拽上传,兼容IE6+及其它标准浏览器,对付一般的上传没有问题,不过如果是上传图片,且需要预览的话,就力有不逮了,趁着闲暇时间,给上传组件添加了单独的图片上传UI,支持图片预览和缩放(通过调整图片的大小以实现图片压缩). 上传组件特点 轻量级,不依赖任何JS库,核心代码(Q.Uploader.js)仅约700行,min版本加起来不到12KB 纯

移动端H5图片上传的bug注意事项分析

上周做一个关于移动端图片压缩上传的功能.期间踩了几个坑,在此总结下. 大体的思路是,部分API的兼容性请参照caniuse: 利用FileReader,读取blob对象,或者是file对象,将图片转化为data uri的形式. 使用canvas,在页面上新建一个画布,利用canvas提供的API,将图片画入这个画布当中. 利用canvas.toDataURL(),进行图片的压缩,得到图片的data uri的值 上传文件. 步骤1当中,在进行图片压缩前,还是对图片大小做了判断的,如果图片大小大于2

js HTML5多图片上传及预览实例解析(不含前端的文件分割)_javascript技巧

本文实例为大家分享了js HTML5多图片上传及预览实例,供大家参考,具体内容如下 主要运用  1.HTML5 files可以选择多文件,以及获取多文件信息  2.XMLHTTPRequest对象,发送HTTP传输请求  3.将每一个文件放在FormData,进行传输  4.利用readAsDataURL将图片内容直接变成url,放在img标签的src当中,生成可预览的图片  html+css+js代码  <!DOCTYPE html> <head> <meta http-e

jQuery+HTML5实现图片上传前预览效果_jquery

本文实例讲述了jQuery+HTML5实现图片上传前预览效果.分享给大家供大家参考.具体如下: 这里主要是使用HTML5 的File API,建立一個可存取到该file的url,一个空的img标签,ID为img0,把选择的文件显示在img标签中,实现图片预览功能.请选择支持HTML API的浏览器,比如谷歌Chrome和火狐等. 运行效果如下图所示: 在线演示地址如下: http://demo.jb51.net/js/2015/jquery-html5-pic-upload-pre-view-c