三种上传文件不刷新页面的方法讨论:iframe/FormData/FileReader

转载自:https://www.cnblogs.com/yincheng/p/5011441.html
发请求有两种方式,一种是用ajax,另一种是用form提交,默认的form提交如果不做处理的话,会使页面重定向。以一个简单的demo做说明:

image.png

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


image.png
document.getElementById("_submit").onclick = function(event){
          //取消掉默认的form提交方式
          if(event.preventDefault) event.preventDefault();
          else event.returnValue = false;                           //对于IE的取消方式
  
          var formDOM = document.getElementsByTagName("form")[0];
          //将form的DOM对象当作FormData的构造函数
          var formData = new FormData(formDOM);
          var req = new XMLHttpRequest();
          req.open("POST", "upload");
          //请求完成
          req.onload = function(){
             if(this.status === 200){
                    //对请求成功的处理
             }
          }
          //将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"));
image.png

如果使用jQuery,可以把formData作为ajax的data参数,同时设置contentType: false和processData: false,告诉jQuery不要去处理请求头和发送的数据。

  看起来这种提交方式跟ajax一样,但是其实并不是完全一样,form提交的数据格式有[三种](http://www.w3school.com.cn/tags/att_form_enctype.asp),如果要上传文件则必须为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中指定](http://www.w3school.com.cn/tags/att_form_target.asp),可以和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的支持性不是很好,可以自行[实现一个](http://stackoverflow.com/questions/4236153/xmlhttprequest-browser-support-for-sendasbinary):

if(typeof XMLHttpRequest.prototype.sendAsBinary === 'undefined'){
XMLHttpRequest.prototype.sendAsBinary = function(text){
var data = new ArrayBuffer(text.length);
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++){
ui8a[i] = (text.charCodeAt(i) & 0xff);
}
this.send(ui8a);
}
}

 代码的关键在于第6行,将字符串转成8位无符号整型,还原二进制文件的内容。在执行了fr.readAsBinaryString之后,二进制文件的内容将会以utf-8的编码以字符串形式存放到result,上面的第6行代码将每个unicode编码转成整型(&0xff或者parseInt),存放到一个8位无符号整型数组里面,第8行把这个数组发送出去。如果直接send,而不是sendAsBinary,服务收到的数据将无法正常还原成原本的文件。

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

     关于FileReader的[支持性](https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader),IE10以上支持,IE9有另外一套File API。

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

个人博客: [http://yincheng.site/js-upload-file](http://yincheng.site/?p=82&preview=true)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容