三種上傳文件不刷新頁面的方法討論 iframe/FormData/FileReader

發請求有兩種方式,一種是用ajax,另外一種是用form提交,默認的form提交若是不作處理的話,會使頁面重定向。以一個簡單的demo作說明:html

html以下所示,請求的路徑action爲"upload",其它的不作任何處理:html5

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

能夠看到默認狀況下,form請求upload的同時重定向到upload。可是不少狀況下是但願form請求像ajax同樣,不會重定向或者刷新頁面。像上面的場景,當上傳完成以後,將用戶選擇的頭像顯示在當前頁面。web

解決辦法第一種是使用html5的FormData,將form裏面的數據封裝到FormData對象裏,而後再以POST的方式send出去。以下面代碼所示,對提交按鈕的單擊事件作一個響應,代碼第6行獲取到form的DOM對象,而後第8行構造一個FormData的實例,第18行,將form數據發送出去。ajax

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按鈕的上方位置顯示上傳的圖片:json

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以下:app

"content-type":"multipart/form-data; boundary=------WebKitFormBoundaryYOE7pWLqdFYSeBFj"

除了multipart/form-data以外,還指定了boundary,這個boundary的做用是用來區分不一樣的字段。因爲FormData對象是不透明的,調用JSON.stringify將會返回一個空的對象{},同時FormData只提供append方法,因此沒法獲得FormData實際上傳的內容,可是能夠經過分析工具或者服務收到的數據進行查看。在上面若是上傳一個文本文件,那麼服務收到的POST數據的原始格式是這樣的:ide

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

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更加靈活。

參考:

1. Ajax Style File Uploading using Hidden iFrame

2. 在web應用中使用文件

3. AJAX File Uploads with the iFrame Method

4. 使用FormData對象

相關文章
相關標籤/搜索