效果图

基础知识

FormData

通过FormData对象可以组装一组用 XMLHttpRequest发送请求的键/值对。它可以更灵活方便的发送表单数据,因为可以独立于表单使用。如果你把表单的编码类型设置为multipart/form-data ,则通过FormData传输的数据格式和表单通过submit() 方法传输的数据格式相同。

这是一种常见的移动端上传方式,FormData也是H5新增的 兼容性如下:

base64

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。 由于2的6次方等于64,所以每6个位元为一个单元,对应某个可打印字符。 三个字节有24个位元,对应于4个Base64单元,即3个字节可表示4个可打印字符。

base64可以说是很出名了,就是用一段字符串来描述一个二进制数据,所以很多时候也可以使用base64方式上传。兼容性如下:

Blob对象

一个 Blob对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式。 File 接口基于Blob,继承 blob功能并将其扩展为支持用户系统上的文件。

简单说Blob就是一个二进制对象,是原生支持的,兼容性如下:

FileReader对象

FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。

FileReader也就是将本地文件转换成base64格式的dataUrl。

图片上传思路

准备工作都做完了,那怎样用这些材料完成一件事情呢。
这里要强调的是,考虑到移动端流量很贵,所以有必要对大图片进行下压缩再上传。
图片压缩很简单,将图片用canvas画出来,再使用canvas.toDataUrl方法将图片转成base64格式。

所以图片上传思路大致是:

1.监听一个input(type=‘file’)的onchange事件,这样获取到文件file;
2.将file转成dataUrl;
3.然后根据dataUrl利用canvas绘制图片压缩,然后再转成新的dataUrl;
4.再把dataUrl转成Blob;
5.把Blob append进FormData中;
6.xhr实现上传。

手机兼容性问题

理想很丰满,现实很骨感。
实际上由于手机平台兼容性问题,上面这套流程并不能全都支持。
所以需要根据兼容性判断。

经过试验发现:

1.部分安卓微信浏览器无法触发onchange事件(第一步就特么遇到问题)
这其实安卓微信的一个遗留问题。 查看讨论 解决办法也很简单:input标签 <input type=“file" name=“image” accept="image/gif, image/jpeg, image/png”>要写成就没问题了。
2.部分安卓微信不支持Blob对象
3.部分Blob对象append进FormData中出现问题
4.iOS 8不支持new File Constructor,但是支持input里的file对象。
5.iOS 上经过压缩后的图片可以上传成功 但是size是0 无法打开。
6.部分手机出现图片上传转换问题。
7.安卓手机不支持多选,原因在于multiple属性根本就不支持。
8.多张图片转base64时候卡顿,因为调用了cpu进行了计算。
9.上传图片可以使用base64上传或者formData上传

上传思路修改方案

经过考虑,我们决定做兼容性处理:

这里边两条路,最后都是File对象append进FormData中实现上传。

代码实现

首先有个html

<input type="file" name="image" accept=“image/*” onchange='handleInputChange'>

然后js如下:

// 全局对象,不同function使用传递数据
const imgFile = {};function handleInputChange (event) {// 获取当前选中的文件const file = event.target.files[0];const imgMasSize = 1024 * 1024 * 10; // 10MB// 检查文件类型if(['jpeg', 'png', 'gif', 'jpg'].indexOf(file.type.split("/")[1]) < 0){// 自定义报错方式// Toast.error("文件类型仅支持 jpeg/png/gif!", 2000, undefined, false);return;}// 文件大小限制if(file.size > imgMasSize ) {// 文件大小自定义限制// Toast.error("文件大小不能超过10MB!", 2000, undefined, false);return;}// 判断是否是iosif(!!window.navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)){// iOStransformFileToFormData(file);return;}// 图片压缩之旅transformFileToDataUrl(file);
}
// 将File append进 FormData
function transformFileToFormData (file) {const formData = new FormData();// 自定义formData中的内容// typeformData.append('type', file.type);// sizeformData.append('size', file.size || "image/jpeg");// nameformData.append('name', file.name);// lastModifiedDateformData.append('lastModifiedDate', file.lastModifiedDate);// append 文件formData.append('file', file);// 上传图片uploadImg(formData);
}
// 将file转成dataUrl
function transformFileToDataUrl (file) {const imgCompassMaxSize = 200 * 1024; // 超过 200k 就压缩// 存储文件相关信息imgFile.type = file.type || 'image/jpeg'; // 部分安卓出现获取不到type的情况imgFile.size = file.size;imgFile.name = file.name;imgFile.lastModifiedDate = file.lastModifiedDate;// 封装好的函数const reader = new FileReader();// file转dataUrl是个异步函数,要将代码写在回调里reader.onload = function(e) {const result = e.target.result;if(result.length < imgCompassMaxSize) {compress(result, processData, false );    // 图片不压缩} else {compress(result, processData);            // 图片压缩}};reader.readAsDataURL(file);
}
// 使用canvas绘制图片并压缩
function compress (dataURL, callback, shouldCompress = true) {const img = new window.Image();img.src = dataURL;img.onload = function () {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0, canvas.width, canvas.height);let compressedDataUrl;if(shouldCompress){compressedDataUrl = canvas.toDataURL(imgFile.type, 0.2);} else {compressedDataUrl = canvas.toDataURL(imgFile.type, 1);}callback(compressedDataUrl);}
}function processData (dataURL) {// 这里使用二进制方式处理dataUrlconst binaryString = window.atob(dataUrl.split(',')[1]);const arrayBuffer = new ArrayBuffer(binaryString.length);const intArray = new Uint8Array(arrayBuffer);const imgFile = this.imgFile;for (let i = 0, j = binaryString.length; i < j; i++) {intArray[i] = binaryString.charCodeAt(i);}const data = [intArray];let blob;try {blob = new Blob(data, { type: imgFile.type });} catch (error) {window.BlobBuilder = window.BlobBuilder ||window.WebKitBlobBuilder ||window.MozBlobBuilder ||window.MSBlobBuilder;if (error.name === 'TypeError' && window.BlobBuilder){const builder = new BlobBuilder();builder.append(arrayBuffer);blob = builder.getBlob(imgFile.type);} else {// Toast.error("版本过低,不支持上传图片", 2000, undefined, false);throw new Error('版本过低,不支持上传图片');}}// blob 转fileconst fileOfBlob = new File([blob], imgFile.name);const formData = new FormData();// typeformData.append('type', imgFile.type);// sizeformData.append('size', fileOfBlob.size);// nameformData.append('name', imgFile.name);// lastModifiedDateformData.append('lastModifiedDate', imgFile.lastModifiedDate);// append 文件formData.append('file', fileOfBlob);uploadImg(formData);
}// 上传图片
uploadImg (formData) {const xhr = new XMLHttpRequest();// 进度监听xhr.upload.addEventListener('progress', (e)=>{console.log(e.loaded / e.total)}, false);// 加载监听// xhr.addEventListener('load', ()=>{console.log("加载中");}, false);// 错误监听xhr.addEventListener('error', ()=>{Toast.error("上传失败!", 2000, undefined, false);}, false);xhr.onreadystatechange = function () {if (xhr.readyState === 4) {const result = JSON.parse(xhr.responseText);if (xhr.status === 200) {// 上传成功} else {// 上传失败}}};xhr.open('POST', '/uploadUrl' , true);xhr.send(formData);
}

多图并发上传

多张图片上传方式有三种:

图片队列一张一张上传
图片队列并发全部上传
图片队列并发上传X个,其中一个返回了结果直接触发下一个上传,保证最多有X个请求。
这个一张一张上传好解决,但是问题是上传事件太长了,体验不佳;多张图片全部上传事件变短了,但是并发量太大了,很可能出现问题;最后这个并发上传X个,体验最佳,只是需要仔细想想如何实现。

并发上传实现

最后我们确定X = 3或者4。比如说上传9张图片,第一次上传个3个,其中一个请求回来了,立即去上传第四个,下一个回来上传第5个,以此类推。
这里我使用es6的generator函数来实现的,定义一个函数,返回需要上传的数组:

*uploadGenerator (uploadQueue) {/*** 多张图片并发上传控制规则* 上传1-max数量的图片* 设置一个最大上传数量* 保证最大只有这个数量的上传请求**/// 最多只有三个请求在上传const maxUploadSize = 3;if(uploadQueue.length > maxUploadSize){const result = [];for(let i = 0; i < uploadQueue.length; i++){// 第一次return maxUploadSize数量的图片if(i < maxUploadSize){result.push(uploadQueue[i]);if(i === maxUploadSize - 1){yield result;}} else {yield [uploadQueue[i]];}}} else {yield uploadQueue.map((item)=>(item));}}

调用的时候:

// 通过该函数获取每次要上传的数组this.uploadGen = this.uploadGenerator(uploadQueue);// 第一次要上传的数量const firstUpload = this.uploadGen.next();// 真正开始上传流程firstUpload.value.map((item)=>{/*** 图片上传分成5步* 图片转dataUrl* 压缩* 处理数据格式* 准备数据上传* 上传** 前两步是回调的形式 后面是同步的形式*/this.transformFileToDataUrl(item, this.compress, this.processData);});

这样将每次上传几张图片的逻辑分离出来。

单个图片上传函数改进

然后遇到了下一个问题,图片上传分成5步,

1.图片转dataUrl
2.压缩
3.处理数据格式
4.准备数据上传
5.上传

这里面前两个是回调的形式,最后一个是异步形式。无法写成正常函数一个调用一个;而且各个function之间需要共享一些数据,之前把这个数据挂载到this.imgFile上了,但是这次是并发,一个对象没法满足需求了,改成数组也有很多问题。

所以这次方案是:第一步创建一个要上传的对象,每次都通过参数交给下一个方法,直到最后一个方法上传。并且通过回调的方式,将各个步骤串联起来。Upload完整的代码如下:

/*** Created by Aus on 2017/7/4.*/
import React from 'react'
import classNames from 'classnames'
import Touchable from 'rc-touchable'
import Figure from './Figure'
import Toast from '../../../Feedback/Toast/components/Toast'
import '../style/index.scss'// 统计img总数 防止重复
let imgNumber = 0;// 生成唯一的id
const getUuid = () => {return "img-" + new Date().getTime() + "-" + imgNumber++;
};class Uploader extends React.Component{constructor (props) {super(props);this.state = {imgArray: [] // 图片已上传 显示的数组};this.handleInputChange = this.handleInputChange.bind(this);this.compress = this.compress.bind(this);this.processData = this.processData.bind(this);}componentDidMount () {// 判断是否有初始化的数据传入const {data} = this.props;if(data && data.length > 0){this.setState({imgArray: data});}}handleDelete(id) {this.setState((previousState)=>{previousState.imgArray = previousState.imgArray.filter((item)=>(item.id !== id));return previousState;});}handleProgress (id, e) {// 监听上传进度 操作DOM 显示进度const number = Number.parseInt((e.loaded / e.total) * 100) + "%";const text = document.querySelector('#text-'+id);const progress = document.querySelector('#progress-'+id);text.innerHTML = number;progress.style.width = number;}handleUploadEnd (data, status) {// 准备一条标准数据const _this = this;const obj = {id: data.uuid, imgKey: '', imgUrl: '', name: data.file.name, dataUrl: data.dataUrl, status: status};// 更改状态this.setState((previousState)=>{previousState.imgArray = previousState.imgArray.map((item)=>{if(item.id === data.uuid){item = obj;}return item;});return previousState;});// 上传下一个const nextUpload = this.uploadGen.next();if(!nextUpload.done){nextUpload.value.map((item)=>{_this.transformFileToDataUrl(item, _this.compress, _this.processData);});}}handleInputChange (event) {const {typeArray, max, maxSize} = this.props;const {imgArray} = this.state;const uploadedImgArray = []; // 真正在页面显示的图片数组const uploadQueue = []; // 图片上传队列 这个队列是在图片选中到上传之间使用的 上传完成则清除// event.target.files是个类数组对象 需要转成数组方便处理const selectedFiles = Array.prototype.slice.call(event.target.files).map((item)=>(item));// 检查文件个数 页面显示的图片个数不能超过限制if(imgArray.length + selectedFiles.length > max){Toast.error('文件数量超出最大值', 2000, undefined, false);return;}let imgPass = {typeError: false, sizeError: false};// 循环遍历检查图片 类型、尺寸检查selectedFiles.map((item)=>{// 图片类型检查if(typeArray.indexOf(item.type.split('/')[1]) === -1){imgPass.typeError = true;}// 图片尺寸检查if(item.size > maxSize * 1024){imgPass.sizeError = true;}// 为图片加上位移idconst uuid = getUuid();// 上传队列加入该数据uploadQueue.push({uuid: uuid, file: item});// 页面显示加入数据uploadedImgArray.push({ // 显示在页面的数据的标准格式id: uuid, // 图片唯一iddataUrl: '', // 图片的base64编码imgKey: '', // 图片的key 后端上传保存使用imgUrl: '', // 图片真实路径 后端返回的name: item.name, // 图片的名字status: 1 // status表示这张图片的状态 1:上传中,2上传成功,3:上传失败});});// 有错误跳出if(imgPass.typeError){Toast.error('不支持文件类型', 2000, undefined, false);return;}if(imgPass.sizeError){Toast.error('文件大小超过限制', 2000, undefined, false);return;}// 没错误准备上传// 页面先显示一共上传图片个数this.setState({imgArray: imgArray.concat(uploadedImgArray)});// 通过该函数获取每次要上传的数组this.uploadGen = this.uploadGenerator(uploadQueue);// 第一次要上传的数量const firstUpload = this.uploadGen.next();// 真正开始上传流程firstUpload.value.map((item)=>{/*** 图片上传分成5步* 图片转dataUrl* 压缩* 处理数据格式* 准备数据上传* 上传** 前两步是回调的形式 后面是同步的形式*/this.transformFileToDataUrl(item, this.compress, this.processData);});}*uploadGenerator (uploadQueue) {/*** 多张图片并发上传控制规则* 上传1-max数量的图片* 设置一个最大上传数量* 保证最大只有这个数量的上传请求**/// 最多只有三个请求在上传const maxUploadSize = 3;if(uploadQueue.length > maxUploadSize){const result = [];for(let i = 0; i < uploadQueue.length; i++){// 第一次return maxUploadSize数量的图片if(i < maxUploadSize){result.push(uploadQueue[i]);if(i === maxUploadSize - 1){yield result;}} else {yield [uploadQueue[i]];}}} else {yield uploadQueue.map((item)=>(item));}}transformFileToDataUrl (data, callback, compressCallback) {/*** 图片上传流程的第一步* @param data file文件 该数据会一直向下传递* @param callback 下一步回调* @param compressCallback 回调的回调*/const {compress} = this.props;const imgCompassMaxSize = 200 * 1024; // 超过 200k 就压缩// 封装好的函数const reader = new FileReader();// ⚠️ 这是个回调过程 不是同步的reader.onload = function(e) {const result = e.target.result;data.dataUrl = result;if(compress && result.length > imgCompassMaxSize){data.compress = true;callback(data, compressCallback); // 图片压缩} else {data.compress = false;callback(data, compressCallback); // 图片不压缩}};reader.readAsDataURL(data.file);}compress (data, callback) {/*** 压缩图片* @param data file文件 数据会一直向下传递* @param callback 下一步回调*/const {compressionRatio} = this.props;const imgFile = data.file;const img = new window.Image();img.src = data.dataUrl;img.onload = function () {const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0, canvas.width, canvas.height);let compressedDataUrl;if(data.compress){compressedDataUrl = canvas.toDataURL(imgFile.type, (compressionRatio / 100));} else {compressedDataUrl = canvas.toDataURL(imgFile.type, 1);}data.compressedDataUrl = compressedDataUrl;callback(data);}}processData (data) {// 为了兼容性 处理数据const dataURL = data.compressedDataUrl;const imgFile = data.file;const binaryString = window.atob(dataURL.split(',')[1]);const arrayBuffer = new ArrayBuffer(binaryString.length);const intArray = new Uint8Array(arrayBuffer);for (let i = 0, j = binaryString.length; i < j; i++) {intArray[i] = binaryString.charCodeAt(i);}const fileData = [intArray];let blob;try {blob = new Blob(fileData, { type: imgFile.type });} catch (error) {window.BlobBuilder = window.BlobBuilder ||window.WebKitBlobBuilder ||window.MozBlobBuilder ||window.MSBlobBuilder;if (error.name === 'TypeError' && window.BlobBuilder){const builder = new BlobBuilder();builder.append(arrayBuffer);blob = builder.getBlob(imgFile.type);} else {throw new Error('版本过低,不支持上传图片');}}data.blob = blob;this.processFormData(data);}processFormData (data) {// 准备上传数据const formData = new FormData();const imgFile = data.file;const blob = data.blob;// typeformData.append('type', blob.type);// sizeformData.append('size', blob.size);// append 文件formData.append('file', blob, imgFile.name);this.uploadImg(data, formData);}uploadImg (data, formData) {// 开始发送请求上传const _this = this;const xhr = new XMLHttpRequest();const {uploadUrl} = this.props;// 进度监听xhr.upload.addEventListener('progress', _this.handleProgress.bind(_this, data.uuid), false);xhr.onreadystatechange = function () {if (xhr.readyState === 4) {if (xhr.status === 200 || xhr.status === 201) {// 上传成功_this.handleUploadEnd(data, 2);} else {// 上传失败_this.handleUploadEnd(data, 3);}}};xhr.open('POST', uploadUrl , true);xhr.send(formData);}getImagesListDOM () {// 处理显示图片的DOMconst {max} = this.props;const _this = this;const result = [];const uploadingArray = [];const imgArray = this.state.imgArray;imgArray.map((item)=>{result.push(<Figure key={item.id} {...item} onDelete={_this.handleDelete.bind(_this)} />);// 正在上传的图片if(item.status === 1){uploadingArray.push(item);}});// 图片数量达到最大值if(result.length >= max ) return result;let onPress = ()=>{_this.refs.input.click();};//  或者有正在上传的图片的时候 不可再上传图片if(uploadingArray.length > 0) {onPress = undefined;}// 简单的显示文案逻辑判断let text = '上传图片';if(uploadingArray.length > 0){text = (imgArray.length - uploadingArray.length) + '/' + imgArray.length;}result.push(<Touchablekey="add"activeClassName={'zby-upload-img-active'}onPress={onPress}><div className="zby-upload-img"><span key="icon" className="fa fa-camera" /><p className="text">{text}</p></div></Touchable>);return result;}render () {const imagesList = this.getImagesListDOM();return (<div className="zby-uploader-box">{imagesList}<input ref="input" type="file" className="file-input" name="image" accept="image/*" multiple="multiple" onChange={this.handleInputChange} /></div>)}
}Uploader.propTypes = {uploadUrl: React.PropTypes.string.isRequired, // 图上传路径compress: React.PropTypes.bool, // 是否进行图片压缩compressionRatio: React.PropTypes.number, // 图片压缩比例 单位:%data: React.PropTypes.array, // 初始化数据 其中的每个元素必须是标准化数据格式max: React.PropTypes.number, // 最大上传图片数maxSize: React.PropTypes.number, // 图片最大体积 单位:KBtypeArray: React.PropTypes.array, // 支持图片类型数组
};Uploader.defaultProps = {compress: true,compressionRatio: 20,data: [],max: 9,maxSize: 5 * 1024, // 5MBtypeArray: ['jpeg', 'jpg', 'png', 'gif'],
};export default Uploader

原文地址: https://segmentfault.com/a/1190000010034177?utm_source=tag-newest

移动端H5实现图片上传相关推荐

  1. android 队列上传图片,话说android端七牛图片上传

    七牛图片上传业务流程如下图(这是官方的图): 由上图可知,要想实现图片上传,是要三端进行交互的(我刚刚开始以为只要七牛服务器跟客户端交互就行) 接下来步骤如下: 1.首先肯定是要有一个七牛的账号,并创 ...

  2. 话说android端七牛图片上传

    七牛图片上传业务流程如下图(这是官方的图): 由上图可知,要想实现图片上传,是要三端进行交互的(我刚刚开始以为只要七牛服务器跟客户端交互就行) 接下来步骤如下: 1.首先肯定是要有一个七牛的账号,并创 ...

  3. h5 实现图片上传 案例

    如何在h5 中实现图片上传 ? (单图片上传) 先写一个按钮 ,通过点击按钮触发文件上传的onclick 事件 <div class="btn" onclick=" ...

  4. 公众号 h5 页面 图片上传 wx.chooseImage使用

    刚开始直接在H5里使用了wx.chooseImage,发现在开发者工具中不断的报错the permission value is offline verifying,慢慢开始搜索才发现在小程序的web ...

  5. android h5选择图片上传,js-微信H5选择多张图片预览并上传(兼容ios,安卓,已测试)...

    值得注意的是: 1.在微信H5中选择图片运用:wx.chooseImage, 上传图片:wx.uploadImage. 2.上传图片的时候务必是一张一张的上传的(建议采用递归) 3.一张图片上传成功后 ...

  6. 基于H5的图片上传解析

    代码实现和解析 一.关于<input type="file" name="" id="file"/> 其files属性记录了你放 ...

  7. h5压缩图片上传 php_一键压缩,图片上传大小不得超过200K?

    今天分享一款压缩图片的小程序,它的名字就叫"图片压缩",多么简单粗暴好理解! 我们在网站实名认证或者上传电子图片的时候,经常被要求上传图片的大小不得超过一定限制,比如:图片不得超过 ...

  8. 移动端开发之图片上传与显示

    1.上传,使用servlet以及ajax (1)需要引入的包: (2)配置web.xml (3)引入servlet的程序 servlet代码: package upload; import java. ...

  9. 一步一步搭建一个图片上传网站(后台服务器用nodejs)

    前几天看了腾讯云社区的一个文件上传的文章 <文件上传那些事儿> ,大体上讲了以下h5中图片上传的几个核心原理,但是没有后端接受的服务器代码,没法做测试.也没有具体的一个实例都是一些基本的原 ...

最新文章

  1. python面向对象编程 Object-Oriented
  2. SELECT语句小结
  3. 要求输入框里面必须同时含有字母,数字,特殊字符,且不小于8位
  4. 三星oneui主屏幕费电_都 9012 年了,三星系统还「负优化」吗?
  5. 微博收藏(机器学习探讨)(二)
  6. SQL SERVER 2005 中的CTE
  7. Paper之ACLEMNLP:2009年~2019年ACL计算语言学协会年会EMNLP自然语言处理的经验方法会议历年最佳论文简介及其解读
  8. 编程实现将rdd转换为dataframe:源文件内容如下(_大数据 什么是RDD?可以干什么?为什么要有RDD?...
  9. ubuntu14.04下apt-get install出现E: Sub-process /usr/bin/dpkg returned an error code 解决方法
  10. pandas 数据分析使用
  11. 从 Android 到 Java:如何从不同视角解决问题?
  12. 小程序 房租水电费记录管理_移民局小程序:中国出入境记录的官方查询利器...
  13. 5.微服务设计 --- 分解单块系统
  14. Python可视化编辑,让Python 不再难懂
  15. 从源码分析HashSet集合
  16. AD7124源码 兼容AD7124-4/8 代码都经过验证 有验证的项目PCB图
  17. 安装谷歌 axure插件
  18. SQL分组排序和排序函数(rank、dense_rank、row_number)
  19. 大屏可视化色彩设计基本知识
  20. webp文件怎么打开?webp压缩工具推荐

热门文章

  1. 万物皆可NFT,UTON NFT正式上线内测
  2. Java开发校招面试考点汇总
  3. 产品经理(12)#竞品调研
  4. 怎样做竞品分析?竞品分析的意义?
  5. html插入swf自动播放,如何在HTML页面中嵌入SWF文件?
  6. mybatis-plus3.5分页插件使用(PaginationInterceptor)
  7. 深圳弘辽科技:淘宝扣分要重视,别捡了芝麻丢了西瓜!
  8. 有一种动物叫做 — 狼
  9. Flutter--Hero组件
  10. audio 静音标签