1. 程式人生 > >人人網首頁拖拽上傳詳解(HTML5 Drag&Drop、FileReader API、FormData)

人人網首頁拖拽上傳詳解(HTML5 Drag&Drop、FileReader API、FormData)

http://fed.renren.com/archives/391

早在公元2011年6月3日傍晚,人人網推出了一個很裝B且完全無視IE瀏覽器的功能——拖拽上床。哦,Sorry, 是拖拽上傳。本文將重點介紹實現拖拽上傳的幾個HTML5技術:Drag&DropFileReader APIFormData

  關於這個拖拽上傳,其實國外有很多網站已經有這樣的應用,最早推出拖拽上傳應用的是Gmail,它支援標準瀏覽器下拖拽本地檔案到瀏覽器中作為郵件的附件傳送。人人網的這個拖拽上傳也是同理,可以讓使用標準瀏覽器的使用者通過簡單的拖拽行為,將本地資料夾中的照片直接上傳到人人網,使用者體驗能得到提升的同時,也希望藉此機會推廣一下標準瀏覽器,淘汰IE。人人網當時也向廣大使用者推出升級瀏覽器活動,並喊出口號:”工欲善其計算機,必先利其瀏覽器”。本次拖拽上傳的宣傳口號:你敢”脫”,我就敢上傳…

人人網 - 拖拽上傳

  言歸正題,首先看看效果,大家如果有人人網帳號的話可以在首頁試一試拖拽上傳功能,下面是演示視訊:

拖拽上傳應用主要使用了以下HTML5技術:

  • Drag&Drop : HTML5基於拖拽的事件機制.
  • File API :  可以很方便的讓Web應用訪問檔案物件,File API包括FileListBlobFileFileReaderURI scheme,本文主要講解拖拽上傳中用到的FileList和FileReader介面。
  • FormData : FormData是基於XMLHttpRequest Level 2的新介面,可以方便web應用模擬Form表單資料,最重要的是它支援檔案的二進位制流資料,這樣我們就能夠通過它來實現AJAX向後端傳送檔案資料了。

HTML5 Drag&Drop 拖拽事件

  關於Drag&Drop拖拽事件,之前我寫過一篇專門介紹的文章《給力的 Google HTML5 訓練營(HTML5 Drag&Drop 拖拽、FileReader例項教程)》,那篇文章詳細講解了Drag & Drap事件的原理和程式碼例項,這裡的拖拽上傳實現原理基本上是一樣的,大家有興趣或不太瞭解的話可以先看看那篇文章,我在這裡就不再過多囉嗦了~下面直接出拖拽上傳簡要程式碼例項:

var oDragWrap = document.body;

//拖進
oDragWrap.addEventListener(‘dragenter’, function(e) {
 e.preventDefault();
}, false);

//拖離
oDragWrap.addEventListener(‘dragleave’, function(e) {
 dragleaveHandler(e);
}, false);

//拖來拖去 , 一定要注意dragover事件一定要清除預設事件
//不然會無法觸發後面的drop事件
oDragWrap.addEventListener(‘dragover’, function(e) {
 e.preventDefault();
}, false);

//扔
oDragWrap.addEventListener(‘drop’, function(e) {
 dropHandler(e);
}, false);

var dropHandler = function(e) {
//將本地圖片拖拽到頁面中後要進行的處理都在這
}

獲取檔案資料 HTML5 File API

  在之前那篇文章中我也有介紹過關於File API中的FileReader介面,作為 File API 的一部分,FileReader 專門用於讀取檔案,根據 W3C 的定義,FileReader 介面 “提供一些讀取檔案的方法與一個包含讀取結果的事件模型”。關於FileReader的詳細介紹和程式碼例項大家可以先去看看那篇文章

  今天我著重介紹一下File API中的FileList介面,它主要通過兩個途徑獲取本地檔案列表,一是<input type=”file”>的表單形式,另一種則是e.dataTransfer.files拖拽事件傳遞的檔案資訊。很顯然,我們這裡會用到後者。

var fileList = e.dataTransfer.files;

  使用files方法將會獲取到拖拽檔案的陣列形勢的資料,每個檔案佔用一個數組的索引,如果該索引不存在檔案資料,將返回null值。可以通過length屬性獲取檔案數量.

var fileNum = fileList.length;

  拖拽上傳需要注意的是需要判斷兩個條件,1:拖拽的是檔案不是頁面中的元素; 2:拖拽的是圖片而不是其它檔案,可以通過file.type屬性獲取檔案的型別

//檢測是否是拖拽檔案到頁面的操作
if (fileList.length === 0) {return;};
//檢測檔案是不是圖片
if (fileList[0].type.indexOf(‘image’) === -1) {return;}

  下面讓我們來看看如何結合之前的拖拽事件來實現拖拽圖片並在頁面中進行預覽:

var dropHandler = function(e) {
 e.preventDefault();

 //獲取檔案列表
 var fileList = e.dataTransfer.files;

 //檢測是否是拖拽檔案到頁面的操作
 if (fileList.length == 0) {return;};

 //檢測檔案是不是圖片
 if (fileList[0].type.indexOf(‘image’) === -1) {return;}

 //例項化file reader物件
 var reader = new FileReader();
 var img = document.createElement(‘img’);

 reader.onload = function(e) {
  img.src = this.result;
  oDragWrap.appendChild(img);
 }
 reader.readAsDataURL(fileList[0]);

}

  這時你如果用FireBug等類似除錯工具檢視DOM的話,會看到<img>標籤的src屬性是一個超長的檔案二進位制資料,所以如果DOM有很多這類圖片,那就要當心瀏覽器效能了,因為這些資料極大地擴充的頁面的程式碼量,而每次頁面的reflow都會對瀏覽器形成很大的負擔,So,如果這些圖片還在DOM中,那就儘量不要做動畫或任何重繪操作,如果真的要做就儘量讓圖片脫離文件流,讓其絕對定位比較靠譜。

補充:可以使用window.URL.createObjectURL(file)來獲取檔案的URL(Chrome下用window.webkitURL.createObjectURL(file)),這種方式獲取的URL要比上面說的readAsDataURL簡短很多。而且可以省去使用FileReader。這裡感謝BinBinLiao的留言建議:) 下面是使用readAsDataURL與createObjectURL生成的程式碼對比:

readasdataurl-vs-createobjecturl

優化後的程式碼:(紅色為優化的程式碼)

var dropHandler = function(e) {
 e.preventDefault();

 var fileList = e.dataTransfer.files;  //獲取檔案列表
 var img = document.createElement(‘img’);

 //檢測是否是拖拽檔案到頁面的操作
 if (fileList.length == 0) {return;};

 //檢測檔案是不是圖片
 if (fileList[0].type.indexOf(‘image’) === -1) {return;}
 

 if (window.URL.createObjectURL) {
  //FF4+
  img.src = window.URL.createObjectURL(fileList[0]);
 } else if (window.webkitURL.createObjectURL) {
  //Chrome8+
  img.src = window.webkitURL.createObjectURL(fileList[0]);
 } else {
  //例項化file reader物件
  var reader = new FileReader();

  reader.onload = function(e) {
   img.src = this.result;
   oDragWrap.appendChild(img);
  }
  reader.readAsDataURL(fileList[0]);
 }

}

  需要注意的是,window.URL.createObjectURL是有生命週期的,也就意味著你每用此方法獲取URL,其生命週期都會和DOM一樣,它會單獨佔用記憶體,所以當刪除圖片或不再需要它是,記得用window.URL.revokeObjectURL(file)來釋放其記憶體。當然,如果你沒有釋放,重新整理頁面也是可以釋放的。

AJAX上傳圖片(file.getAsBinary & FormData)

  既然已經獲取到了拖拽到web頁面中圖片的資料,下一步就是將其傳送到伺服器端了。

  話說HTML5時代之前,AJAX傳輸檔案二進位制流資料是不可能完成的事情,而現在我們完全可以通過file.getAsBinary獲取檔案的二進位制資料流,進而將其當做XHR的data資料傳送到後端,8過由於Chrome不支援file的getAsBinary方法,FF3.6+支援此方法。所以Chrome就要另尋它法了,這時我們發現XMLHttpRequest Level 2中的FormData介面完美解決了這個問題,它可以很快捷的模擬Form表單資料並通過AJAX傳送至後端,FormData的支援情況是FF5及以上支援,Chrome12及以上支援。

   file.getAsBinary獲取檔案流很簡單,但是要想上傳資料,就要模擬一下表單的資料格式了,首先看看模擬表單的js程式碼, FormData模擬表單資料時更是簡潔,不用麻煩的去拼字串,而是直接將資料append到formdata物件中即可:

var xhr = new XMLHttpRequest();
var url = ‘http://upload.renren.com/……’;
var boundary = ‘———————–’ + new Date().getTime();
var fileName = file.name;

xhr.open(“post”, url, true);
xhr.setRequestHeader(‘Content-Type’, ‘multipart/form-data; boundary=’ + boundary);

if (window.FormData) {
 //Chrome12+
 var formData = new FormData();
 formData.append(‘file’, file);
 formData.append(‘hostid’, userId);
 formData.append(‘requestToken’, t);

 data = formData;
} else if (file.getAsBinary) {
 //FireFox 3.6+
 data = “–” +
 boundary +
 crlf +
 ”Content-Disposition: form-data; ” +
 ”name=\”" +
 ’file’ +
 ”\”; ” +
 ”filename=\”" +
 unescape(encodeURIComponent(file.name)) +
 ”\”" +
 crlf +
 ”Content-Type: image/jpeg” +
 crlf +
 crlf +
 file.getAsBinary() +
 crlf +
 ”–” +
 boundary +
 crlf +
 ”Content-Disposition: form-data; ” +
 ”name=\”hostid\”" +
 crlf +
 crlf +
 userId +
 crlf +
 ”–” +
 boundary +
 crlf +
 ”Content-Disposition: form-data; ” +
 ”name=\”requestToken\”" +
 crlf +
 crlf +
 t +
 crlf +
 ”–” +
 boundary +
 ’–’;
}

xhr.send(data);

首先表單資料headers頭資訊需要以下兩項:

  • Content-Type : 設定其為multipart/form-data來模擬表單資料
  • boundary : 表單資料中的分隔符,用於分隔不同的檔案或表單項,這是伺服器端設定的格式。

傳送時的post資料類似這樣:

————————-1323611763556
Content-Disposition: form-data; name=”file”; filename=”4.jpg”
Content-Type: image/jpeg

ÿØÿà�JFIF�…這裡是檔案二進位制流…~iúoî­5P%-vãîHü 4QHgÿÙ
————————-1323611763556
Content-Disposition: form-data; name=”hostid”

229421603
—————————–1323612996486

Content-Disposition: form-data; name=”requestToken”

369009193
————————-1323611763556–

好了,現在檔案上傳成功後你就可以按照平常AJAX的操作來進行後續處理了。

最後,再來總結一下拖拽上傳的技術要點:

  1. 監聽拖拽:監聽頁面元素的拖拽事件,包括:dragenter、dragover、dragleave和drop,一定要將dragover的預設事件取消掉,不然無法觸發drop事件。如需拖拽頁面裡的元素,需要給其新增屬性draggable=”true”;
  2. 獲取拖拽檔案:在drop事件觸發後通過e.dataTransfer.files獲取拖拽檔案列表,.length屬性獲取檔案數量,.type屬性獲取檔案型別。
  3. 讀取圖片資料並新增預覽圖:例項化FileReader物件,通過其readAsDataURL(file)方法獲取檔案二進位制流,並監聽其onload事件,將e.result賦值給img的src屬性,最後將圖片append到DOM中。
  4. 傳送圖片資料:使用file.getAsBinaryFormData分別模擬表單資料AJAX提交檔案流。