目标:
实现一个无刷新的,多图片上传控件.(目前已在91美图网中使用)
特点:
采用渐进增强的设计思路,针对支持HTML5新特性比较好的现代浏览器,使用HTML5中的新特性,包括File
对象,XMLHttpRequest中的upload对象,File对象等新增的功能实现较为高级的多图片无刷新能够检测上传进度的上传控件.而对于不支持HTML5
特性的较老的浏览器则使用传统的隐藏iFrame
的形式来实现伪装的多图片上传功能.
优缺点分析:
对于支持HTML5的现代浏览器,可以通过原生的input
控件添加multiple属性实现文件的多选上传.这种方法可以使用XMLHttpReqeust
对象直接上传图片对象,使用较为方便,而且可以在本地获取图片上传的进度.但是目前占市场份额较多的IE浏览器包括IE9以及低于IE9版本的IE浏览器均不支持这项新内容.
所以对于不支持HTML5中的multiple
属性的input[type='file']
的浏览器这里采用了隐藏iframe,将待提交的表单的target设置为iframe
的name,以此来实现变通的无刷新图片上传.事实上,这种方法并不是没有刷新,而是把刷新的操作放到iframe中了而已.由于较旧的浏览器事实上并不支持上传控件的多选,所以这里只能通过多次选择,选中多个文件,操作较为复杂.而且本方法要想实现上传进度必须配合服务器端的查询,增加服务压力.
实现效果:
实现:
首先通过能力检查的方式判断浏览器是否支持HTML5中的新特性,如果支持的话选择HTML5的上传方式,不支持则使用传统的方式实现.
isSupportMultipleFile: window.File && window.FileReader && window.FileList && window.Blob,
在使用HTML5的新功能上传时用的主要内容有:1.<input type="file" />
控件的新属性multiple
,添加了此属性的控件支持同时选择多个文件,在选择完成后会自动生成一个File
对象的集合FileList
对象.这里我们就是通过监控input控件的onchange事件,来获取用户选中的文件.
File对象包括的主要属性:
- fileName-字符串,文件的名称
- fileSize-无符号的长整型,文件的大小的bytes
- name-字符串,文件名
- size-无符号长整型,File对象引用的文件的大小(bytes)
- type-字符串,File对象引用文件的类型(MIME type)
在获取到FileList之后根据每个File对象的内容生成界面中的待上传项目:
addUploadItem: function(guid, filename, percent){}
本方法主要是根据唯一编号,文件名称和当前进度生成一个待上传的条目,用于界面显示,主要操作为DOM操作,这里不再赘述.
接下来就是最主要的文件上传部分的内容了,主要的工作就是:当用户点击上传按钮时,将缓存起来的FileList遍历上传.
这里我采用了每次同时上传5个文件的策略,同时上传的文件太多会造成浏览器崩溃,以及服务器端阻塞超时的情况.具体同时上传文件数目可以根据实际情况决定.
这里要说明一下,使用XMLHttpReqeust对象上传File对象有两种方法:
- 直接使用XMLHttpRequest将File对象Post到服务器端,但是由于这样发送的内容没有有效的请求信息,只是将文件内容Post到服务器端,所以在服务器端无法通过正常的post获取文件内容,所以要自己读取服务器到的源内容来获取图片.
PHP
中可以通过$GLOBALS["HTTP_RAW_POST_DATA"]
来获取,ASP.NET中可以通过Request.BinaryRead(Request.TotalBytes)
来获取,其他语言您可以自行查找.当然文件的其他属性,比如大小,名称等等可以通过设置请求头的方式来实现,XMLHttpRequest.setReqeustHeader();
- 另外Mozilla Developer NetWork上提供了另外一种方法,即通过FileReader对象来获取File的二进制字符串,然后自己按照标准的结构构造一个请求,通过
XMLHttpReqeust.sendAsBinary
来以二进制流的形式上传,来实现后端服务器像普通模式一样获取内容.但是很遗憾,XMLHttpReqeust.sendAsBinary
这个方法是FireFox私有的方法,标准中没有,其他浏览器也并没有支持,所以经过测试能用后我果断放弃了这个方法.
需要了解的技术内容已经清晰了,下面就来看一下实现吧,我用伪代码+注释的形式来说明:
//获取缓存好的FileList对象 var allFiles = fileuploader.cache.files; //新建一个迭代游标用于控制当前同时上传的文件数目 var iterator = 0; //上传的主要方法 var upload = function(){ //检查如果FileList对象的数目为0则直接退出 if(allFiles.length == 0){ return; } //如果FileList不为空,且当前的迭代游标小于5则执行上传 while(allFiles.length > 0 && iterator < 5){ //弹出一个File var file = allFiles.shift(); //游标加1 iterator++; //TODO:获取界面上的条目,并设置状态为开始上传 var perDom = fileuploader.uploadItems[file.guid]; /*样式设置代码省略*/ //新建XMLHttpRequest上传File文件,通过一个自执行的匿名函数将perDom传给异步请求,留作请求完成使用 (function(perDom){ var req = new Request({ url: '/member/img-item/ajax-upload', dataType: 'json', method: 'post', success: function(data){ //检查数据的状态,并处理返回的数据 if(data.status){ fileuploader.addImages([data]); //TODO:设置状态为成功 /**样式设置代码省略**/ }else{ //TODO:设置状态为失败 /**样式设置代码省略**/ } //游标减1表示上传完成 iterator--; //检查是否全部上传完毕 if(allFiles.length == 0){ fileuploader.close(); } //如果游标变为0则重新调用upload方法上传剩余的内容 if(iterator == 0){ upload(); } }, progress: function(e){ //上传过程中计算上传的进度 var percent = parseInt(100 * e.loaded / e.total); perDom.progressBar.style.width = percent + '%'; } }); //设置请求头信息,包括文件的名称大小类型唯一标识等等 req.setRequestHeader("DKUPLOADER_NAME", file.name); req.setRequestHeader("DKUPLOADER_SIZE", file.size); req.setRequestHeader("DKUPLOADER_TYPE", file.type); req.setRequestHeader("DKUPLOADER_GUID", file.guid); //上传文件 req.send(file); })(perDom) } };
使用HTML5上传图片的内容到这里就结束了.怎么样很简单是不是.下面就继续说一下传统的方法上传的实现.
普通的基于iframe的无刷新文件上传系统已经是老生常谈的问题了,即将Form的target指向iframe,在执行submit的时候,页面不会刷新,只是iframe刷新.只要页面返回的内容中执行javascript调用父窗口的相关对象来更新状态即可.
当然本身这种方法是无法实现获取文件上传进度的,但是在php环境中,通过Alternative PHP Cache(APC)模块的扩展带来的小惊喜,可以获取文件上传的进度.前提是你需要独立安装这个扩展,而且需要你通过脚本定时查询后台来获取上传进度.这么做一是进度不够精确,另外增加服务器负担,除非是文件体积较大,否则实在是没有太大的必要这么做.至于其他的后台语言我没有深入研究,如果有哪位愿意分享还请多多指教.
这里实现选择多个文件是通过将<input type="file"/>
注册onchange
事件,待其选择文件后将其移动到另外一个隐藏容器中并将其缓存起来.而在原来上传按钮的位置重新创建一个新的input对象.这在点击选择文件按钮时会一直有上传选择窗口弹出,来实现伪装的多文件上传.
是实际上传的时候,<input type="file"/>
移动的策略跟第一种情况基本类似,只不过上传文件的方法由XMLHttpRequest改为Form的Submit方法.下面继续已代码说明:
var upload = function(){ //获取所有缓存的input[type='file']对象 var allFiles = fileuploader.cache.files; //如果数目为零则跳出 if(allFiles.length <= 0){ fileuploader.close(); return; } //迭代游标 var iterator = 0; var uploadForm = fileuploader.dom.uploadForm; var uploadItems = fileuploader.uploadItems; //先清空form对象内部的内容 uploadForm.innerHTML = ''; //限定一次上传最多5个文件 while(allFiles.length > 0 && iterator < 5){ var file = allFiles.shift(); iterator++; var perDom = uploadItems[file.guid]; //修改状态为正在上传 perDom.status.className = 'message'; perDom.status.innerHTML = "正在上传"; perDom.thumb.innerHTML = '<img src="http://www.91meitu.net/resource/images/ajax_loader_s.gif" />'; //将input[type='file']对象移动到待上传的表单内部 uploadForm.appendChild(file); } //开始上传 uploadForm.submit(); }
当然利用iframe跟使用HTML5上传还是有些区别的,因为使用Form提交的内容,通过javascript是无法直接获取上传的状态的,所以要被动等待iframe的内容返回后,通过调用方法来实现完成上传后的下一步操作.
我这里的图片上传控件是一个静态的对象,名字叫fileuploader,所以在iframe中调用的时候可以通过调用fileupload.complete方法来实现图片上传完成后的下一步操作.页面返回的内容是:
<script>window.parent.fileuploader.complete(" . json_encode($result) . ")</script>
而complete方法的具体内容如下:
var complete = function(){ var filesInfo = []; for(var i = 0, len = data.length; i < len; i++){ var imgData = data[i]; var perDom = fileuploader.uploadItems[imgData.guid]; if(imgData.status){ filesInfo.push(imgData); //TODO:设置状态为成功 /**样式设置代码省略**/ }else{ //TODO:设置状态为失败 /**样式设置代码省略**/ } } fileuploader.addImages(filesInfo); //继续上传剩余的文件 this.iframeupload(); }
写到这基本上整个上传控件就写完了,当然还有一些open(),close()方法,一些自定义事件什么的都可以继续丰富和完善.抛砖引玉,欢迎不吝指教!
使用方法,在页面载入完毕后执行fileuploader.open();
文件中使用了我自己写的一个小工具包,打包成zip文件,欢迎大家下载源代码.
点击下载:fileuploader源代码
参考资料: