谈谈基于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 相关文章推荐
用js+xml自动生成表格的东西
Dec 21 Javascript
Javascript调试工具(下载)
Jan 09 Javascript
Jquery中getJSON在asp.net中的使用说明
Mar 10 Javascript
跨浏览器的事件对象介绍
Jun 27 Javascript
Bootstrap零基础学习第一课之模板
Jul 18 Javascript
JavaScript中windows.open()、windows.close()方法详解
Jul 28 Javascript
Vue2.0实现购物车功能
Jun 05 Javascript
新手vue构建单页面应用实例代码
Sep 18 Javascript
JS实现区分中英文并统计字符个数的方法示例
Jun 09 Javascript
vue在App.vue文件中监听路由变化刷新页面操作
Aug 14 Javascript
Vue实现简易购物车页面
Dec 30 Vue.js
vue3中provide && inject的使用
Jul 01 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
攻克CakePHP系列三 表单数据增删改
2008/10/22 PHP
ThinkPHP中自定义错误页面和提示页面实例
2014/11/22 PHP
php-msf源码详解
2017/12/25 PHP
Yii框架参数配置文件params用法实例分析
2019/09/11 PHP
Laravel 中使用简单的方法跟踪用户是否在线(推荐)
2019/10/30 PHP
ExtJS Store的数据访问与更新问题
2010/04/28 Javascript
Jquery+WebService 校验账号是否已被注册的代码
2010/07/12 Javascript
JavaScript中的几个关键概念的理解-原型链的构建
2011/05/12 Javascript
JavaScript闭包 懂不懂由你反正我是懂了
2011/10/21 Javascript
js解析与序列化json数据(二)序列化探讨
2013/02/01 Javascript
Node.js开发指南中的简单实例(mysql版)
2013/09/17 Javascript
原生js实现fadein 和 fadeout淡入淡出效果
2014/06/05 Javascript
使用js dom和jquery分别实现简单增删改
2014/09/11 Javascript
javascript实现获取字符串hash值
2015/05/10 Javascript
详解原生JavaScript实现jQuery中AJAX处理的方法
2016/05/10 Javascript
jQuery简单实现列表隐藏和显示效果示例
2016/09/12 Javascript
在bootstrap中实现轮播图实例代码
2017/06/11 Javascript
Vue 动态设置路由参数的案例分析
2018/04/24 Javascript
jQuery实现每隔一段时间自动更换样式的方法分析
2018/05/03 jQuery
js统计页面上每个标签的数量实例代码
2018/05/29 Javascript
微信小程序实现联动选择器
2019/02/15 Javascript
layer实现登录弹框,登录成功后关闭弹框并调用父窗口的例子
2019/09/11 Javascript
[55:45]LGD vs OG 2019国际邀请赛淘汰赛 胜者组 BO3 第三场 8.24
2019/09/10 DOTA
python初学者,用python实现基本的学生管理系统(python3)代码实例
2019/04/10 Python
在Pytorch中使用样本权重(sample_weight)的正确方法
2019/08/17 Python
PyCharm专业最新版2019.1安装步骤(含激活码)
2019/10/09 Python
英国床垫在线:Mattress Online
2016/12/07 全球购物
JustFab加拿大:女鞋、靴子、手袋和服装在线
2018/05/18 全球购物
马来西亚综合购物网站:Lazada马来西亚
2018/06/05 全球购物
单位单身证明范本
2014/01/11 职场文书
宣传部部长竞选演讲稿
2014/04/26 职场文书
青奥会口号
2014/06/12 职场文书
中学生秋季运动会广播稿
2014/09/21 职场文书
拾金不昧表扬稿大全
2015/05/05 职场文书
HTML中的表单Form实现居中效果
2021/05/25 HTML / CSS
Python实现批量自动整理文件
2022/03/16 Python