zoukankan      html  css  js  c++  java
  • 三种上传文件不刷新页面的方法讨论:iframe/FormData/FileReader

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

          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数据发送出去。

     1     document.getElementById("_submit").onclick = function(event){
     2           //取消掉默认的form提交方式
     3           if(event.preventDefault) event.preventDefault();
     4           else event.returnValue = false;                           //对于IE的取消方式
     5   
     6           var formDOM = document.getElementsByTagName("form")[0];
     7           //将form的DOM对象当作FormData的构造函数
     8           var formData = new FormData(formDOM);
     9           var req = new XMLHttpRequest();
    10           req.open("POST", "upload");
    11           //请求完成
    12           req.onload = function(){
    13              if(this.status === 200){
    14                     //对请求成功的处理
    15              }
    16           }
    17           //将form数据发送出去
    18           req.send(formData);
    19       //避免内存泄漏
    20       req = null;
    21 }

          上传成功后,服务将返回图片的访问地址,补充14行对请求成功的处理:在submit按钮的上方位置显示上传的图片:

    1                 var img = document.createElement("img");
    2                 img.src = JSON.parse(this.responseText).path;
    3                 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发出去的字符串一模一样的内容,然后再作参数解析,所以就得统一参数的格式:

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

     1         var req = new XMLHttpRequest();
     2         req.open("POST", "upload");
     3         //设置和邮箱一样的Content-Type
     4         req.setRequestHeader("Content-Type", "application/octet-stream");
     5         var fr = new FileReader();
     6         fr.onload = function(){
     7             req.sendAsBinary(this.result);
     8         }
     9         req.onload = function(){
    10                 //一样,省略
    11         }
    12   //读取input文件内容,放到fileReader的result字段里
    13 fr.readAsBinaryString(this.form["file"].files[0]);

          代码第13行执行读文件,读取完毕后触发第6行的load响应函数,第7行以二进制文本形式发送出去。由于sendAsBinary的支持性不是很好,可以自行实现一个

     1   if(typeof XMLHttpRequest.prototype.sendAsBinary === 'undefined'){
     2       XMLHttpRequest.prototype.sendAsBinary = function(text){
     3       var data = new ArrayBuffer(text.length);
     4       var ui8a = new Uint8Array(data, 0);
     5       for (var i = 0; i < text.length; i++){ 
     6           ui8a[i] = (text.charCodeAt(i) & 0xff);
     7       }
     8       this.send(ui8a);
     9     }
    10   }    

         代码的关键在于第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更加灵活。

    个人博客: http://yincheng.site/js-upload-file

    参考:

    1. Ajax Style File Uploading using Hidden iFrame

    2. 在web应用中使用文件

    3. AJAX File Uploads with the iFrame Method

    4. 使用FormData对象

    讨论知识,更讨论思维方式
  • 相关阅读:
    序列
    2018131
    成都七中
    NOIP2017
    洛谷P1352 CodeVS1380 没有上司的舞会
    BZOJ1087 SCOI2005 互不侵犯King
    11-4-2017 星期六 R-Day?
    11-3-2017 星期五
    11-2-2017 星期四
    USACO 2014 US Open, Silver Problem 2. Dueling GPSs
  • 原文地址:https://www.cnblogs.com/yincheng/p/5011441.html
Copyright © 2011-2022 走看看