
html如下所示,请求的路径action为"upload",其它的不做任何处理:
<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",演示如下:

可以看到默认情况下,form请求upload的同时重定向到upload。但是很多情况下是希望form请求像ajax一样,不会重定向或者刷新页面。像上面的场景,当上传完成之后,将用户选择的头像显示在当前页面。
解决办法第一种是使用html5的FormData,将form里面的数据封装到FormData对象里,然后再以POST的方式send出去。如下面代码所示,对提交按钮的单击事件做一个响应,代码第6行获取到form的DOM对象,然后第8行构造一个FormData的实例,第18行,将form数据发送出去。
document.getElementById("_submit").onclick = function(event){ //取消掉默认的form提交方式 if(event.preventDefault) event.preventDefault(); else event.returnValue = false; //对于IE的取消方式 var formDOM = document.getElementsByTagName("form")[]; //将form的DOM对象当作FormData的构造函数 var formData = new FormData(formDOM); var req = new XMLHttpRequest(); req.open("POST", "upload"); //请求完成 req.onload = function(){if(this.status === ){//对请求成功的处理} } //将form数据发送出去 req.send(formData); //避免内存泄漏 req = null; } 上传成功后,服务将返回图片的访问地址,补充14行对请求成功的处理:在submit按钮的上方位置显示上传的图片: var img = document.createElement("img"); img.src = JSON.parse(this.responseText).path; 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如下:
"content-type":"multipart/form-data; boundary=------WebKitFormBoundaryYOE7pWLqdFYSeBFj"
除了multipart/form-data之外,还指定了boundary,这个boundary的作用是用来区分不同的字段。由于FormData对象是不透明的,调用JSON.stringify将会返回一个空的对象{},同时FormData只提供append方法,所以无法得到FormData实际上传的内容,但是可以通过分析工具或者服务收到的数据进行查看。在上面如果上传一个文本文件,那么服务收到的POST数据的原始格式是这样的:
------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发出去的字符串一模一样的内容,然后再作参数解析,所以就得统一参数的格式:
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");//放到documentthis.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邮箱: var req = new XMLHttpRequest(); req.open("POST", "upload"); //设置和邮箱一样的Content-Type req.setRequestHeader("Content-Type", "application/octet-stream"); var fr = new FileReader(); fr.onload = function(){req.sendAsBinary(this.result); } req.onload = function(){ //一样,省略 } //读取input文件内容,放到fileReader的result字段里 fr.readAsBinaryString(this.form["file"].files[0]); 代码第13行执行读文件,读取完毕后触发第6行的load响应函数,第7行以二进制文本形式发送出去。由于sendAsBinary的支持性不是很好,可以自行实现一个: if(typeof XMLHttpRequest.prototype.sendAsBinary === "undefined"){XMLHttpRequest.prototype.sendAsBinary = function(text){var data = new ArrayBuffer(text.length);var uia = new UintArray(data, );for (var i = ; i < text.length; i++){uia[i] = (text.charCodeAt(i) & xff);}this.send(uia);} } 代码的关键在于第6行,将字符串转成8位无符号整型,还原二进制文件的内容。在执行了fr.readAsBinaryString之后,二进制文件的内容将会以utf-8的编码以字符串形式存放到result,上面的第6行代码将每个unicode编码转成整型(&0xff或者parseInt),存放到一个8位无符号整型数组里面,第8行把这个数组发送出去。如果直接send,而不是sendAsBinary,服务收到的数据将无法正常还原成原本的文件。form.html<form enctype="multipart/form-data" method="post" target="upload" action="upload.php" > <input type="file" name="uploadfile" /><input type="submit" /> </form> <iframe name="upload" style="display:none"></iframe><!--和一般的<form>标签相比多了一个target属性罢了,用于指定标签页在哪里打开以及提交数据。
--> upload.php<?phpheader("Content-type:text/html;charset=utf-");class upload{ public $_file; public function __construct(){if(!isset($_FILES["uploadfile"])){ $name=key($_FILES);}if(!isset($_FILES["uploadfile"])){ throw new Exception("并没有文件上传"); }$this->_file=$_FILES["uploadfile"]; //$this->_file一维数组var_dump($this->_file);//判断文件是否是通过 HTTP POST 上传的//如果 filename 所给出的文件是通过 HTTP POST 上传的则返回 TRUE。这可以用来确保恶意的用户无法欺骗脚本去访问本不能访问的文件,例如 /etc/passwd。 if(!is_uploaded_file($this->_file["tmp_name"]))throw new Exception("异常情况"); if($this->_file["error"] !== )throw new Exception("错误代码:".$this->_file["error"]);} public function moveTo($new_dir){$real_dir=$this->checkDir($new_dir)."/";$real_dir=str_replace("\","/",$real_dir);if(!move_uploaded_file($this->_file["tmp_name"],$real_dir.$this->_file["name"])){ exit("上传失败");}echo "<script type="text/javascript">alert("上传成功")</script>"; } public function checkDir($dir){if(!file_exists($dir)){ mkdir($dir,,true);}return realpath($dir);}}$upload=new upload();$new_dir="./a/b";$upload->moveTo($new_dir);