发请求有两种方式,一种是用ajax,另外一种是用form提交,默认的form提交若是不作处理的话,会使页面重定向。以一个简单的demo作说明:html
html以下所示,请求的路径action为"upload",其它的不作任何处理:html5
<form method="POST" action="upload" enctype="multipart/form-data"> 名字 <input type="text" name="user"></input> 头像 <input type="file" name="file"></input> <input type="submit" id="_submit" value="提交"></input> </form>
服务端(node)response直接返回: "Recieved form data",演示以下:node
能够看到默认状况下,form请求upload的同时重定向到upload。可是不少状况下是但愿form请求像ajax同样,不会重定向或者刷新页面。像上面的场景,当上传完成以后,将用户选择的头像显示在当前页面。web
解决办法第一种是使用html5的FormData,将form里面的数据封装到FormData对象里,而后再以POST的方式send出去。以下面代码所示,对提交按钮的单击事件作一个响应,代码第6行获取到form的DOM对象,而后第8行构造一个FormData的实例,第18行,将form数据发送出去。ajax
1 document.getElementById("_submit").onclick = function(event){ 2 //取消掉默认的form提交方式 3 if(event.preventDefault) event.preventDefault(); 4 else event.returnValue = false; //对于IE的取消方式 5 6 var formDOM = document.getElementsByTagName("form")[0]; 7 //将form的DOM对象看成FormData的构造函数 8 var formData = new FormData(formDOM); 9 var req = new XMLHttpRequest(); 10 req.open("POST", "upload"); 11 //请求完成 12 req.onload = function(){ 13 if(this.status === 200){ 14 //对请求成功的处理 15 } 16 } 17 //将form数据发送出去 18 req.send(formData); 19 //避免内存泄漏 20 req = null; 21 }
上传成功后,服务将返回图片的访问地址,补充14行对请求成功的处理:在submit按钮的上方位置显示上传的图片:json
1 var img = document.createElement("img"); 2 img.src = JSON.parse(this.responseText).path; 3 formDOM.insertBefore(img, document.getElementById("_submit"));
示例:数组
若是使用jQuery,能够把formData做为ajax的data参数,同时设置contentType: false和processData: false,告诉jQuery不要去处理请求头和发送的数据。 安全
看起来这种提交方式跟ajax同样,可是其实并非彻底同样,form提交的数据格式有三种 ,若是要上传文件则必须为multipart/form-data,因此上面的form提交请求里的http的头信息里面的Content-Type为multipart/form-data,而普通的ajax提交为application/json。form提交完整的Content-Type以下:app
"content-type":"multipart/form-data; boundary=------WebKitFormBoundaryYOE7pWLqdFYSeBFj"
除了multipart/form-data以外,还指定了boundary,这个boundary的做用是用来区分不一样的字段。因为FormData对象是不透明的,调用JSON.stringify将会返回一个空的对象{},同时FormData只提供append方法,因此没法获得FormData实际上传的内容,可是能够经过分析工具或者服务收到的数据进行查看。在上面若是上传一个文本文件,那么服务收到的POST数据的原始格式是这样的:ide
------WebKitFormBoundaryYOE7pWLqdFYSeBFj Content-Disposition: form-data; name="user" abc ------WebKitFormBoundaryYOE7pWLqdFYSeBFj Content-Disposition: form-data; name="file"; filename="test.txt" Content-Type: text/plain 这是一个文本文件的内容。 ------WebKitFormBoundaryYOE7pWLqdFYSeBFj--
从上面服务收到的数据看出FormData提交的格式,每一个字段以boundary隔开,最后以--结束。而ajax请求,send出去的数据格式是自定义的,通常都是以key=value中间用&链接:
var req = new XMLHttpRequest(); var sendData = "user=abc&file=这是一个文本文件的内内容"; req.open("POST", "upload"); //发送的数据须要转义,见上面提到的三种格式 req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); req.send(sendData);
服务就会收到和send发出去的字符串如出一辙的内容,而后再做参数解析,因此就得统一参数的格式:
user=abc&file=这是一个文本文件的内容
从这里能够看出POST本质上并不比GET安全,POST只是没有将数据放在网址传送而已。
考虑到FormData到了IE10才支持,若是要支持较低版本的IE,那么能够借助iframe。
文中一开始就说,默认的form提交会使页面重定向,而重定向的规则在 target中指定 ,能够和a标签同样指定为"_blank",在新窗口中打开;还能够指定为一个iframe,在该iframe中打开。因此能够弄一个隐藏的iframe,将form的target指向这个iframe,当form请求完成时,返回的数据就会由这个iframe显示,正如上面在新页面显示的:"Recieved form data"。请求完成后,iframe加载完成,触发load事件,在load事件的处理函数里,获取该iframe的内容,从而拿到服务返回的数据了!拿到后再把iframe删掉。
在提交按钮的响应函数里,首先建立一个iframe,设置iframe为不可见,而后再添加到文档里:
var iframe = document.createElement("iframe"); iframe.width = 0; iframe.height = 0; iframe.border = 0; iframe.name = "form-iframe"; iframe.id = "form-iframe"; iframe.setAttribute("style", "width:0;height:0;border:none"); //放到document this.form.appendChild(iframe);
改变form的target为iframe的name值:
this.form.target = "form-iframe";
而后再响应iframe的load事件:
iframe.onload = function(){ var img = document.createElement("img"); //获取iframe的内容,即服务返回的数据 var responseData = this.contentDocument.body.textContent || this.contentWindow.document.body.textContent; img.src = JSON.parse(responseData).path; f.insertBefore(img, document.getElementById("_submit")); //删掉iframe setTimeout(function(){ var _frame = document.getElementById("form-iframe"); _frame.parentNode.removeChild(_frame); }, 100); //若是提示submit函数不存在,请注意form里面是否有id/value为submit的控件 this.form.submit(); }
第二种办法到这里就基本能够了,可是若是看163邮箱或者QQ邮箱上传文件的方式,会发现和上面的两种方法都不太同样。用httpfox抓取请求的数据,会发现上传的内容的格式并非上面说的用boundary隔开,而是直接把文件的内容POST出去了,而文件名、文件大小等相关信息放在了文件的头部。如163邮箱:
POST Data: this is a text Headers: Mail-Upload-name: content.txt Mail-Upload-size: 15
能够推测它们应该是直接读取了input文件的内容,而后直接POST出去了。要实现这样的功能,能够借助FileReader,读取input文件的内容,再保留二进制的格式发送出去:
1 var req = new XMLHttpRequest(); 2 req.open("POST", "upload"); 3 //设置和邮箱同样的Content-Type 4 req.setRequestHeader("Content-Type", "application/octet-stream"); 5 var fr = new FileReader(); 6 fr.onload = function(){ 7 req.sendAsBinary(this.result); 8 } 9 req.onload = function(){ 10 //同样,省略 11 } 12 //读取input文件内容,放到fileReader的result字段里 13 fr.readAsBinaryString(this.form["file"].files[0]);
代码第13行执行读文件,读取完毕后触发第6行的load响应函数,第7行以二进制文本形式发送出去。因为 sendAsBinary 的支持性不是很好,能够自行 实现一个 :
1 if(typeof XMLHttpRequest.prototype.sendAsBinary === 'undefined'){ 2 XMLHttpRequest.prototype.sendAsBinary = function(text){ 3 var data = new ArrayBuffer(text.length); 4 var ui8a = new Uint8Array(data, 0); 5 for (var i = 0; i < text.length; i++){ 6 ui8a[i] = (text.charCodeAt(i) & 0xff); 7 } 8 this.send(ui8a); 9 } 10 }
代码的关键在于第6行,将字符串转成8位无符号整型,还原二进制文件的内容。在执行了fr.readAsBinaryString以后,二进制文件的内容将会以utf-8的编码以字符串形式存放到result,上面的第6行代码将每一个unicode编码转成整型(&0xff或者parseInt),存放到一个8位无符号整型数组里面,第8行把这个数组发送出去。若是直接send,而不是sendAsBinary,服务收到的数据将没法正常还原成本来的文件。
上面的实现须要考虑文件太大,需分段上传的问题。
关于FileReader的 支持性 ,IE10以上支持,IE9有另一套File API。
文章讨论了3种办法实现无刷新上传文件,分别是使用iframe、FormData和FileReader,支持性最好是的iframe,可是从体验的效果来看FormData和FileReader更好,由于这二者不用生成一个无用的DOM再删除,其中FormData最简单,而FileReader更加灵活。
参考:
1. Ajax Style File Uploading using Hidden iFrame
2. 在web应用中使用文件
3. AJAX File Uploads with the iFrame Method
4. 使用FormData对象