上传文件已经是个已经成熟的前端技术,目前开源的拿来即用的前端上传插件也比较多,诸如:Web Uploader、JSAjaxFIleUploader、
jQuery-File-Upload,通常这些上传插件包含的功能有:选择上传、支持拖拽、MD5校验、图片预览、上传进度显示等功能;
这篇文章主要分析讨论前端上传控件的功能实现原理,以及上传功能如何做到功能的渐进式增强。
文件上传方式
文件上传最原始的方式form元素表单提交,发展后form原始+iframe实现异步文件上传,到后来HTML5出现ajax实现文件上传。所以通常上传控件向下兼容的方案通常是高版本浏览器采用ajax方式,低版本浏览器采用iframe+form表单形式。
form表单提交
<form id="j-puload-form" action="/fileUpload" method="post" enctype="multipart/form-data">
<input type="file" id="j-upload-input" name="upload"/><button type="submit">提交</button>
</form>
form表单属性中action属性规定后端处理文件上传的路径;method属性规定上传文件的方法post or get;enctype属性规定在发送到服务器之前应该如何对表单数据进行编码,在使用包含文件上传控件的表单时必须使用“multipart/form-data”。
iframe封装form表单
使用form元素比较简单,但缺点也比较明显:上传同步、上传完成页面会刷新;
在HTML5出现之前,想要实现文件异步上传,只能通过iframe+form实现;
实现方式
原理:文件上传时在页面中动态创建一个iframe元素和一个form元素,并将form元素的target属性指向动态创建iframe元素。当用户完成选择文件动作时,提交子页面中的 form。这时,iframe跳转,而父页面没有刷新。这使得上传结束后,服务器处理结果返回到动态iframe窗口而没有刷新页面;
<input type="file" id="j-upload-input" name="upload"/>
var createUploadForm = function (id, fileElementId) {
//create form
var formId = 'jUploadForm' + id;
var fileId = 'jUploadFile' + id;
var form = $('<form action="" method="POST" name="' + formId + '" id="' + formId + '" enctype="multipart/form-data"></form>');
var oldElement = $('#' + fileElementId);
var newElement = $(oldElement).clone();
$(oldElement).attr('id', fileId);
$(oldElement).before(newElement);
$(oldElement).appendTo(form);
$(form).css('position', 'absolute');
$(form).css('top', '-1200px');
$(form).css('left', '-1200px');
$(form).appendTo('body');
return form;
}
var createUploadIframe = function (id) {
//create frame
var frameId = 'jUploadFrame' + id;
var iframeHtml = '<iframe id="' + frameId + '" name="' + frameId + '" style="position:absolute; top:-9999px; left:-9999px"' + ' src="' + '" />';
$(iframeHtml).appendTo(document.body);
return jQuery('#' + frameId).get(0);
}
var actionURL = "/fileUpload";
$('#j-upload-input').change(function () {
var id = new Date().getTime() ;
var frameId = 'jUploadFrame' + id;
var formId = 'jUploadForm' + id;
var form = createUploadForm(id, "j-upload-input");
var frame = createUploadIframe(id);
form.appendTo(document.body);
var form = $('#' + formId);
$(form).attr('action', actionURL);
$(form).attr('method', 'POST');
$(form).attr('target', frameId);
$(form).attr('enctype', 'multipart/form-data');
$(form).submit();
})
上述程序实现了,id值为“j-upload-input”的input元素,在触发文件选择时(onchange事件),动态创建一个form元素和一个iframe元素,input加入一个动态创建form元素,并将form元素的target值指向iframe元素,最终结果实现了触发input文件选择,发送文件请求,但是页面不刷新;
结果处理
通过iframe+form上传,上传结果处理需要前后端配合;
1.前后端预先约定好回调函数名;
例如,在当前页面中定义好上传的回调函数。
function uploadCallBack (resp){...}
服务返回的数据形式可以为:
<script type="text/javascript">
window.top.window['uploadCallBack'](resp);
</script>
通过window.top.window[uploadCallBack]可以调用到iframe父级元素中定义的uploadCallBack方法,也就是预先定义的回调处理;
2.前端页可以监听frame 的onLoad确定是否请求超时和后端是否给予返回;
通过FormData ajax方式
XMLHttpRequest Level 2添加了一个新的接口FormData利用FormData对象,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()
方法来异步的提交这个"表单"。比起普通的ajax,使用FormData
的最大优点就是我们可以异步上传一个二进制文件。
构建一个FormData并上传文件
var xhr = new XMLHttpRequest();
var formData = new FormData();
for (var key in params) {
formData.append(key, params[key]);
}
formData.append(fileName, fileObj);
xhr.open(this.options.method, this.options.url, true);
xhr.send(formData);
通过拖拽操作选择文件
现在很多上传功能都包含拖拽上传,实现上传功能首先要创建一个拖放操作的目的区域并应用程序的设计来决定哪部分的内容接受 drop;
var dragArea;
if ((dragArea = document.getElementById("j-drag-area")) && dragArea.addEventListener) {
dragArea.addEventListener("dragover", dragoverHandler, false);
dragArea.addEventListener("dragleave", dragleaveHandler, false);
dragArea.addEventListener("drop", dropHandler, false);}
在例子中定义了id值为“j-drag-area”的元素为文件拖拽上传受理区域,我们需要在该元素上绑定 dragover,dragleave,和drop 事件。
其中dragover,当拖拽中的鼠标移动经过一个元素的时候触发,可以做一些文件经过,拖拽区域高亮处理。dragleave当拖拽中的鼠标离开元素时触发。监听器需要将作为可释放反馈的高亮或插入标记去除。drop
这个事件在拖拽操作结束释放时于释放元素上触发。一个监听器用来响应接收被拖拽的数据并插入到释放之地。
function dragoverHandler(event) {
event.stopPropagation();
event.preventDefault();
......
//这里可以添加拖拽区域背景高亮处理样式
}
function dragleaveHandler(event) {
event.stopPropagation();
event.preventDefault();
......
//这里可以异常拖拽区域背景高亮处理的样式
}
function dropHandler(event) {
event.stopPropagation();
event.preventDefault();
//获取并处理文件
var dt = event.dataTransfer;
var files = dt.files;
handleFiles(files);
}
在代码中的event.dataTransfer.files属性表示被拖动到浏览器窗口中的文件列表。
文件上传进度
XMLHttpRequest Level 2中,传送数据的时候,有一个progress事件,上传数据progress事件属于XMLHttpRequest.upload对象,上传数据过程中会触发。事件回调函数中可以使用事件event的下列属性:event.total是需要传输的总字节;event.loaded是已经传输的字节;如果event.lengthComputable不为真,则event.total等于0。
var xhr = new XMLHttpRequest(),
formData = new FormData();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {// 4 = "loaded"
onComplete(xhr);//上传完成处理 }};
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
onProgressHandler( e.loaded, e.total, xhr);
//e.total是需要传输的总字节,e.loaded是已经传输的字节。但如果e.lengthComputable值为false,则e.total等于0。
// 通过(e.loaded/e.total)即可得到上传比例,可以用这个已上传比例去更新进度条啦
}
};
xhr.open(this.options.method, this.options.url, true);
for (var key in params) {
formData.append(key, params[key]);
}
formData.append(fileName, fileObj);
xhr.send(formData);
对于低版本浏览器则可以用通过轮询的方式获取上传进度;
文件MD5
HTML5 DOM新增的File API,使得JavaScript操作文件成为可能;
要在浏览器中对文件进行md5,基本思路就是使用HTML5的FileReader接口把文件读取到内存,然后获取文件的二进制内容,最后再进行md5。
读取文件
file = document.getElementById("file").files[0];
文件切割
//file的slice方法,注意它的兼容性,在不同浏览器的写法不同
blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice
//然后指定file和开始结束的片段,就可以得到切割的文件了。
blobSlice.call(file, start, end);
计算文件MD5
spark = new SparkMD5();
spark.appendBinary(filepice1);
spark.appendBinary(filepice2);
spark.appendBinary(filepice3);
....//所有的分片处理好之后调用下面的方法就能获取到文件的MD5了
spark.end()
附上js-spark-md5计算文件MD5方法 Demo源码
document.getElementById('file').addEventListener('change', function () {
var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
file = this.files[0],
chunkSize = 2097152, // Read in chunks of 2MB
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(),
fileReader = new FileReader();
fileReader.onload = function (e) {
console.log('read chunk nr', currentChunk + 1, 'of', chunks);
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
console.log('finished loading');
console.info('computed hash', spark.end());
// Compute hash
}
};
fileReader.onerror = function () {
console.warn('oops, something went wrong.');
};
function loadNext() {
var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
图片预览
如果上传的文件是图片类型,上传插件通常会提供图片预览功能,图片预览首先要判断文件类型是否为图片类型,可以通过正则表达式匹配判断
var imageType = /^image\//;
if ( imageType.test(file.type) ) {
//是图片;
}
读取和显示图片,首先要构建一个img元素标签,给img的src属性赋值;读取图片文件可用new FileReader()对象的readAsDataURL(file)方法,方法返回文件的base64编码串。
例子:
html
<input type="file" onchange="previewFile()"><br>
<img src="" height="200" alt="Image preview...">
function previewFile() {
var preview = document.querySelector('img');
var file = document.querySelector('input[type=file]').files[0];
var reader = new FileReader();
reader.addEventListener("load", function () {
preview.src = reader.result;
}, false);
if (file) {
reader.readAsDataURL(file);
}
}
参考:
FormData
Using XMLHttpRequest
HTML5 file api 读取文件MD5码
文件上传的渐进式增强
在web应用中使用文件
拖放操作
在浏览器端获取文件的MD5值
js-spark-md5