HTML5上传一般是异步,会使用到FormData构建需要递交的文件数据。
最原始的获取文件并上传(不做任何本地校验、过滤等处理),仅需要在input.onchange的时候读取fileList对象添加到FormData中递交即可。
假设仅上传一个文件:
<input type="file" id="upload" />
var uploadField = document.getElementById('upload');
uploadField.addEventListener('change', function () {
var file = this.files[0];
var fileData = new FormData();
formdata.append('files', file, file.name);
// POST伪代码
doPost(url, fileData, callback)
// 清理当次选择的的文件记录,见备注2
this.value = null;
})
备注:
无论选择多少个文件,返回的都是fileList,仅上传一个文件的话需要fileList[0]
读取
change
事件自身会记录上次选择的文件,不做处理的话选择同一个(一批)文件并不会触发。如果不需要这个特性,则需要手动设置this.value = null
如果不清理文件记录,已经选择过文件的情况下,再次选择文件但是主动退出,那么依旧会返回一个空的fileList,注意做过滤,否则this.files[0] === undefined
fileList[i]具有以下可读属性,如果需要做校验等处理会用到:
HTML部分中,input支持多选multiple
及文件格式过滤accept
属性。但是需要注意,多选情况下设置accept不要用通配符,会造成触发选择文件时的卡顿。比如:
<input type="file" multiple accept="image/*" />
最好写成
<input type="file" multiple accept="image/jpg, image/jpeg, image/png, image/gif" />
accept
属性仅能在用户默认行为下进行过滤,当用户自行切换文件选择方式时,此规则会被绕过,所以 完整流程需要对fileList进行过滤!
图片上传场景,一般包括以下三种:
直接上传即基础部分,不再说明。
本地预览图片,需要用到FileReader读取图片信息,把file对象转为base64data。img
标签支持src的值为base64data字符串。
<input type="file" id="upload" />
<div id="preview"></div>
var uploadFiled = document.getElementById('upload');
var previewArea = document.getElementById('preview');
uploadFiled.addEventListener('change', function () {
var file = this.files[0];
var reader = new FileReader();
var dataOfImage;
reader.onload = function () {
dataOfImage = reader.result;
previewArea.innerHTML = '<img src="' + dataOfImage + '" />';
};
reader.readAsDataURL(file);
this.value = null;
})
大多实际场景,产品汪都会要求限定上传图片的尺寸,那么就需要想办法获取原始图片的宽高信息。以往一般通过flash检测,现在则可以通过Image.onload来获取图片信息:
var maxWidth = 800;
var maxHeight = 800;
reader.onload = function () {
var image = new Image();
var dataOfImage = reader.result;
var imageWidth;
var imageHeight;
image.onload = function () {
imageWidth = this.naturalWidth;
imageHeight = this.naturalHeight;
if ((imageWidth < maxWidth) || (imageHeight < maxHeight)) {
previewArea.innerHTML = '<img src="' + dataOfImage + '" />';
} else {
window.alert('图片尺寸应小于 ' + maxWidth + ' x ' + maxHeight);
}
};
image.src = dataOfImage;
};
大多数图片都可以通过上面两步完成。但是有些图片,比如 这张 和 这张 (下载到本地试验),会发现和在电脑上预览的不同,发生了翻转。这是因为iOS设备拍的照片会自带镜头方向信息,电脑上预览会自动修正到正常方向,但是在网页中并没有这种智能处理,这需要我们手动完成。
要完成检测及处理,需要用到两个组件:
exif.js用来读取图片文件的翻转信息Orientation。返回值为数字或者undefined,直接读取input.onchange返回的file对象,不需要读取转换过的base64data:
var orientation;
EXIF.getData(file, function () {
orientation = EXIF.getTag(this, 'Orientation');
});
注:exif.js仅支持读取.jpg
和.tiff
的信息,其他格式图片是没有exif信息的,返回undefined。
ios-imagefile-megapixel.js则相对复杂,这个插件会通过canvas读取原图,根据前者拿到的Orientation,在新的中转canvas上重新排列每个像素点,之后 把结果绘制在指定的img元素、Image对象或者Canvas 上。注意暂时没发现这个插件提供直接导出base64data的方法,需要指定好绘制对象。一般使用render方法:
var mpImg = new MegaPixImage(file);
mpImg.render(targetImage, { orientation: 1, quality: 0.8 });
直接读取file对象,传入通过exif.js拿到的Orientation值,并设置导出jpeg时希望的压缩比例。几种不同场景示例:
<img id="image" />
var drawImgWithMegapix = function (file, orientation, quality) {
var mpImg = new MegaPixImage(file);
var targetImage = document.getElementById('image');
mpImg.render(targetImage, { orientation: orientation, quality: quality });
};
<canvas id="canvas"></canvas>
var drawCanvasWithMegapix = function (file, orientation, quality) {
var mpImg = new MegaPixImage(file);
var targetCanvas = document.getElementById('canvas');
mpImg.render(targetCanvas, { orientation: orientation, quality: quality });
};
这种方法可以避免DOM中提前插入<img src="" />
所带来的一些问题:
在这里,render方法相当于设置了image.src:
<p id="imgWrapper"></p>
var setImageWithMegapix = function (file, orientation, quality) {
var mpImg = new MegaPixImage(file);
var imageObj = new Image();
var imgWrapper = document.getELementById('imgWrapper');
imageObj.onload = function () {
// 前置过滤blabla
// 把image对象插入DOM节点。注意image对象不是DOM节点,不能使用innerHTML。
// 允许重复选择文件的话,注意插入前先清空旧节点!
imgWrapper.appendChild(imgaeObj);
// 对图片进行后续设定
imgWrapper.querySelector('img').width = '500';
};
mpImg.render(imageObj, { orientation: orientation, quality: quality });
};
备注:
<img />
或new Image()
时,图片的src都为base64data基础部分的上传,直接把file对象塞进FormData发送即可,但是对于iOS图片进行过处理的,则需要考虑上传的是处理过翻转的本地图片还是源文件。这取决于公司自己的后端程序情况及产品需求:
ios-imagefile-megapixel处理过的图片已经抹掉了exif信息。类似摄影之类网站,不仅对照片拍摄时的相机参数等原始信息有保存需求,很可能还需要提供原始文件下载,这需要保留源文件上传,而不是处理过的文件。这种情况下,如果后端有类似处理方案,直接上传源文件即可;如果后端没有,则需要同时上传双份文件。
如果产品对源文件信息无需求,那么前端只需把处理过的图片上传。
本地处理过的图片不再是file对象,而是base64data,这里需要转化为二进制供上传:
var base64ToBlob = function (base64Data, imageType) {
// imageType为保存文件的格式字符串,如'image/jpeg'。
// 一般保持和源文件格式相同,通过input.change的时候读取file[i].type即可
var blobOfBase64Data;
var imgStringArray;
var blob;
var i;
blobOfBase64Data = base64Data.split(',')[1];
blobOfBase64Data = window.atob(blobOfBase64Data);
imgStringArray = new Uint8Array(blobOfBase64Data.length);
for (i = 0; i < blobOfBase64Data.length; i++) {
imgStringArray[i] = blobOfBase64Data.charCodeAt(i);
}
blob = new Blob([imgStringArray], { type: imageType });
return blob;
};
base64ToBlob(base64Data, imageType)
处理后的对象,可以像input.onchange获取的file对象一样,通过FormData.append()
直接构造表单内容。
裁剪多用于头像等场合,一般结合拖动等操作。和普通的预览上传相比,需要通过canvas实现,主要用到两个API:
canvas.getContext('2d').drawImage()
提供把图片来源(<img />
或者另一个canvas
)的一部分绘制到canvas上的功能,即裁剪canvas.toDataURL()
提供把canvas转化为base64data的功能,方便最终转换和递交理论上,一个canvas就可以完成功能,大致流程为:
<img id="originImg" />
<canvas id="canvas"></canvas>
<img id="previewImg" />
// 原始图片,已经通过其他函数获取到图片文件并赋值了src
var originImg = document.getElementById('originImg');
// 预览图片,供生成预览图片用
var previewImg = document.getElementById('previewImg');
// 裁剪出的区域相对于原图的坐标及尺寸
var clippedWidth;
var clippedHeight;
var clippedX;
var clippedY;
// 拖动选区交互过程中,计算出前面四个参数
// ...blablabla
var canvas = document.getElementById('canvas');
var canvasCtx = canvas.getContext('2d');
// canvas最终需要导出成图片,所以宽高设置成和裁剪出的区域一致
canvas.width = clippedWidth;
canvas.height = clippedHeight;
canvasCtx.drawImage(
originImg, clippedX, clippedY, clippedWidth, clippedHeight,
0, 0, clippedWidth, clippedHeight
)
// canvas.toDataURL()参数为图片格式字符串及压缩率,同上文base64ToBlob方法。返回值为base64data
previewImg.setAttribute('src', canvas.toDataURL(imageType, 0.8));
// 后续的对toDataURL()返回的base64data处理及上传等操作
// ...blablabla
这样的处理,可以 工作,但在拖拽中需要频繁绘制和导出图片,会带来一系列问题:
<img>
标签的问题canvas.toDataURL()
的性能并不高,裁剪类交互大多都有独立的递交按钮,实际上只需要最终递交的时候执行一次即可实际场景中,为了性能或者产品需求,可能用到多个canvas:
仅仅用来1:1保存原始图片信息,以及最终导出裁剪后的图片数据使用
document.createElement('canvas')
生成,不插入DOM树originImageCanvas.toDataURL
导出最终图片信息即可。使用canvas可以避免<img src="">
带来的问题。canvasCtx.drawImage()
可以直接读取另一个canvas,不再牵涉原始图片本身。
用作数据中转,可省略,但是建议使用,方便处理previewCanvas的尺寸问题。以及,已获得的资料显示,游戏开发等经常使用 离屏canvas 来解决性能问题。
document.createElement('canvas')
生成,不插入DOM树总体流程为:
canvas绘图,如果绘制了一张小于canvas本体大小的内容进去,导出后的图片会带有黑边。
一般情况下,显示原始图片的区域都是根据UI固定的,显示区域比原始图片小的话,原始图片就需要缩放进去显示。这时候,拖动选区对应到最终实际图片上的裁剪区域,就会遇到比例换算及浮点数取整的问题。
比如,一张300x400的原始图片,原始图预览区域是160x160,拖动区70x70,按照默认的图片和拖动区都居中,那么缩放后的原始图为120 x 160,拖动区域的60x60换算过来对应原图裁剪出来区域的则是
{
width: 175, // 70 / (160/400)
height: 175, // 70 / (160/400)
x: 62.5, // ((120 - 70) / 2) / (160 / 400),
y: 112.5, // ((160 - 70) / 2) / (160 / 400),
}
而实际中的原始图片大多数并不是相对容易换算的300x400,所得出的小数部分只会更复杂。
另外,出于性能考虑,canvas计算需要避免使用浮点数坐标。
最早一版使用正则校验:
var isImageReg = /(.*)+\.(jpg|jpeg|gif|png)$/i;
if (isImageReg.test(imageName) === false) {
// 异常操作
}
但是发现文件名过长且 文件名不匹配 时,浏览器会卡死。最终换成常规的基于字符串拼接的校验。
var allowFiletypeList = ['jpg', 'jpeg', 'gif', 'png'];
var isImageFile = function (fileName) {
var fileTypeString = fileName.split('.').pop().toLowerCase();
return (allowFiletypeList.indexOf(fileTypeString) > -1);
};
比如裁剪头像等业务场景,经常会有初始状态的“选择图片”和更换图片的“换一张”按钮,两者都可以触发选图。但是两者的files对象是相互独立的,在做相同文件排查等场合就需要额外处理。
如果有需求,可以使用统一的input标签,界面层想办法在所有用到的地方触发此input的click事件,比如jQuery的.trigger()
方法,或者直接input.click()
。
这里有个性能问题,直接input.click()触发,浏览器有时候会卡顿。也可以使用另一种方法变通实现:
<label><input type="file" /></label>
或者
<label for="inputField"></label>
<input type="file" id="inputField" />
这是利用了HTML原生的特性,label可以和input绑定,只需要在外部调用label.click()
即可。这样同一业务的所有上传都使用同一控件,方便进行files对象管理。
常规的移动端浏览器中,图片上传和处理方式和Web端基本一致,但是有不少细节差异。
移动端的图片选择,不只是相册中文件一个途径,还多了相机直接拍摄等,所以交互从Web端的单步文件选择窗口变为两步,并且 不可人工干预:
选择渠道
选择文件
注意第一张图,里面多了个摄像机的选项。这里和Web端不同,Web端为了降级文件筛选时的消耗,会把指定的文件具体列出来:
<input type="file" accept="image/jpg, image/jpeg, image/png, image/gif" />
而移动端里,某些浏览器不一定支持多规则,要不显示摄像头,只能使用通配符写法:
<input type="file" accept="image/*" />
并且,单独指定某一类型的文件也是无效的,比如指定只选择jpg格式文件:
<!-- 依旧可以选择到jpg之外的文件,等同于accept="image/*" -->
<input type="file" accept="image/jpg" />
这意味着,移动端要过滤掉摄像机,可能只有设定选择所有图片文件,拿到图片后再做类型过滤。
<input type="file" accept="image/*" multiple>
很遗憾,文件多选的功能,在Android上不能生效。参见caniuse的统计
(测试环境:Google Nexus 5,Android 6.0.1,Chrome 55.0.2883.91)
微信环境中,普通的HTML5选择文件可以使用,但是不能直接定位到微信APP自己内部整理的相册。以我的手机为例,要选择微信的图片,需要进入“照片/图片”,然后选择“Weixin“文件夹。
微信官方提供的有JSSDK,可以调用APP功能,绕过常规HTML5的交互和多文件选择限制。
具体功能参见微信JSSDK官网文档
需要额外说明的是,微信JSSDK拿到的图片,如果要通过canvas做裁剪,必须走一套完整的流程:
因为通过JSSDK选取图片,拿到的路径是微信APP定义的在图片系统中的路径协议 weixin://
,这对于canvas来说跨域了。
这样的完整流程,需要额外注意loading的设计及处理,因为耗费在网络传输上的时间太多了。
if updatedAt !== null p 最后更新时间: 2017-02-17 08:00:00