谈谈基于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 相关文章推荐
Prototype Date对象 学习
Jul 12 Javascript
JS实现的数组全排列输出算法
Mar 19 Javascript
PHP+jQuery实现随意拖动层并即时保存拖动位置
Apr 30 Javascript
jquery实现Slide Out Navigation滑出式菜单效果代码
Sep 07 Javascript
JavaScript常用字符串与数组扩展函数小结
Apr 24 Javascript
简单谈谈JS数组中的indexOf方法
Oct 13 Javascript
详解vue 数组和对象渲染问题
Sep 21 Javascript
JS实现给数组对象排序的方法分析
Jun 24 Javascript
使用Webpack提升Vue.js应用程序的4种方法(翻译)
Oct 09 Javascript
Angular 多模块项目构建过程
Feb 13 Javascript
原生Javascript+HTML5一步步实现拖拽排序
Jun 12 Javascript
一篇文章告诉你如何实现Vue前端分页和后端分页
Feb 18 Vue.js
解决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处理会话函数大总结
2015/08/05 PHP
php+iframe 实现上传文件功能示例
2020/03/04 PHP
setTimeout的延时为0时多个浏览器的区别
2012/05/23 Javascript
各种常用的JS函数整理
2013/10/25 Javascript
jquery xMarquee实现文字水平无缝滚动效果
2014/04/29 Javascript
JavaScript异步加载浅析
2014/12/28 Javascript
浅谈jQuery中的事件
2015/03/23 Javascript
JS实现控制表格行文本对齐的方法
2015/03/30 Javascript
原生JavaScript实现Ajax的方法
2016/04/07 Javascript
Javascript之BOM(window对象)详解
2016/05/25 Javascript
同步异步动态引入js文件的几种方法总结
2016/09/23 Javascript
使用JavaScript获取Request中参数的值方法
2016/09/27 Javascript
微信开发 JS-SDK 6.0.2 经常遇到问题总结
2016/12/08 Javascript
EditPlus 正则表达式 实战(3)
2016/12/15 Javascript
JS实现页面打印功能
2017/03/16 Javascript
vue.js项目打包上线的图文教程
2017/11/16 Javascript
node基于puppeteer模拟登录抓取页面的实现
2018/05/09 Javascript
vue-cli 首屏加载优化问题
2018/11/06 Javascript
HTML元素拖拽功能实现的完整实例
2020/12/04 Javascript
解决vue使用vant轮播组件swipe + flex时文字抖动问题
2021/01/07 Vue.js
vue-video-player 断点续播的实现
2021/02/01 Vue.js
python实现各进制转换的总结大全
2017/06/18 Python
linux查找当前python解释器的位置方法
2019/02/20 Python
我用Python抓取了7000 多本电子书案例详解
2019/03/25 Python
Python 、Pycharm、Anaconda三者的区别与联系、安装过程及注意事项
2019/10/11 Python
PyQt5+python3+pycharm开发环境配置教程
2020/03/24 Python
python 多线程死锁问题的解决方案
2020/08/25 Python
python sleep和wait对比总结
2021/02/03 Python
html5本地存储之localstorage 、本地数据库、sessionStorage简单使用示例
2014/05/08 HTML / CSS
儿科护士实习自我鉴定
2013/10/17 职场文书
优秀体育委员自荐书
2014/01/31 职场文书
语文高效课堂实施方案
2014/05/03 职场文书
中学教师师德师风演讲稿
2014/08/22 职场文书
一份没有按时交货失信于客户的检讨书
2014/09/19 职场文书
Spring Bean的实例化之属性注入源码剖析过程
2021/06/13 Java/Android
关于对TypeScript泛型参数的默认值理解
2022/07/15 Javascript