谈谈基于iframe、FormData、FileReader三种无刷新上传文件的方法


Posted in Javascript onDecember 03, 2015

发请求有两种方式,一种是用ajax,另一种是用form提交,默认的form提交如果不做处理的话,会使页面重定向。以一个简单的demo做说明:

谈谈基于iframe、FormData、FileReader三种无刷新上传文件的方法

     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",演示如下:

谈谈基于iframe、FormData、FileReader三种无刷新上传文件的方法

       可以看到默认情况下,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"));

      示例: 

谈谈基于iframe、FormData、FileReader三种无刷新上传文件的方法

      如果使用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发出去的字符串一模一样的内容,然后再作参数解析,所以就得统一参数的格式:

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文件的内容,再保留二进制的格式发送出去: 

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,服务收到的数据将无法正常还原成原本的文件。

     上面的实现需要考虑文件太大,需分段上传的问题。

    关于FileReader的支持性,IE10以上支持,IE9有另外一套File API。

     文章讨论了3种办法实现无刷新上传文件,分别是使用iframe、FormData和FileReader,支持性最好是的iframe,但是从体验的效果来看FormData和FileReader更好,因为这两者不用生成一个无用的DOM再删除,其中FormData最简单,而FileReader更加灵活。

面给大家介绍iframe无刷新上传文件

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属性罢了,用于指定标签页在哪里打开以及提交数据。

如果没有设置该属性,就会像平常一样在本页重定向打开action中的url。

而如果设置为iframe的name值,即"upload"的话,就会在该iframe内打开,因为CSS设置为隐藏,因而不会有任何动静。若将display:none去掉,还会看到服务器的返回信息。 

--> 

upload.php
<?php
header("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);
Javascript 相关文章推荐
jQuery 事件队列调整方法
Sep 18 Javascript
基于jquery的一个简单的脚本验证插件
Apr 05 Javascript
JavaScript的document对象和window对象详解
Dec 30 Javascript
JavaScript中的面向对象介绍
Jun 30 Javascript
一个js导致的jquery失效问题的解决方法
Nov 27 Javascript
JavaScript怎么判断图片是否加载完成以便获取其尺寸
May 08 Javascript
node.js中的fs.fchmodSync方法使用说明
Dec 16 Javascript
jQuery实现的简洁下拉菜单导航效果代码
Aug 26 Javascript
jQuery插件cxSelect多级联动下拉菜单实例解析
Jun 24 Javascript
ZeroClipboard.js使用一个flash复制多个文本框
Jun 19 Javascript
js实现canvas图片与img图片的相互转换的示例
Aug 31 Javascript
详解node+express+ejs+bootstrap构建项目
Sep 27 Javascript
解决JavaScript数字精度丢失问题的方法
Dec 03 #Javascript
Javascript实现检测客户端类型代码封包
Dec 03 #Javascript
javascript学习小结之prototype
Dec 03 #Javascript
简单实现JS对dom操作封装
Dec 02 #Javascript
jQuery实现获取绑定自定义事件元素的方法
Dec 02 #Javascript
JS折半插入排序算法实例
Dec 02 #Javascript
如何动态加载外部Javascript文件
Dec 02 #Javascript
You might like
PHP中创建并处理图象
2006/10/09 PHP
PHP中对数据库操作的封装
2006/10/09 PHP
PHP实现一个轻量级容器的方法
2019/01/28 PHP
laravel框架语言包拓展实现方法分析
2019/11/22 PHP
基于jquery的划词搜索实现(备忘)
2010/09/14 Javascript
关于onScroll事件在IE6下每次滚动触发三次bug说明
2011/09/21 Javascript
JS 排序输出实现table行号自增前端动态生成的tr
2014/08/13 Javascript
JavaScript检查某个function是否是原生代码的方法
2014/08/20 Javascript
谈谈AngularJs中的隐藏和显示
2015/12/09 Javascript
jQuery实时显示鼠标指针位置和键盘ASCII码
2016/03/28 Javascript
微信小程序 前端源码逻辑和工作流详解
2016/10/08 Javascript
js制作支付倒计时页面
2016/10/21 Javascript
JavaScript数据结构之广义表的定义与表示方法详解
2017/04/12 Javascript
Vue结合后台导入导出Excel问题详解
2019/02/19 Javascript
Jquery如何使用animation动画效果改变背景色的代码
2020/07/20 jQuery
[06:21]完美世界亚洲区首席发行官竺琦TI3采访
2013/08/26 DOTA
Python学习笔记(二)基础语法
2014/06/06 Python
彻底搞懂Python字符编码
2018/01/23 Python
对python numpy.array插入一行或一列的方法详解
2019/01/29 Python
Python实现FM算法解析
2019/06/18 Python
python中多个装饰器的调用顺序详解
2019/07/16 Python
Django自带的加密算法及加密模块详解
2019/12/03 Python
python搜索算法原理及实例讲解
2020/11/18 Python
matplotlib交互式数据光标实现(mplcursors)
2021/01/13 Python
canvas学习总结三之绘制路径-线段
2019/01/31 HTML / CSS
介绍一下代理模式(Proxy)
2014/10/17 面试题
会计主管岗位职责范文
2013/11/08 职场文书
企业总经理职责
2014/02/02 职场文书
渔夫的故事教学反思
2014/02/14 职场文书
服务承诺书范文
2014/05/19 职场文书
建筑学专业自荐书
2014/07/09 职场文书
给老婆的道歉信
2015/01/20 职场文书
清明节扫墓活动总结
2015/02/09 职场文书
班干部学习委员竞选稿
2015/11/20 职场文书
2016年11月份红领巾广播稿
2015/12/21 职场文书
Java界面编程实现界面跳转
2022/06/16 Java/Android