人人網(wǎng)首頁拖拽上傳詳解

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

  關(guān)于這個(gè)拖拽上傳,其實(shí)國外有很多網(wǎng)站已經(jīng)有這樣的應(yīng)用,最早推出拖拽上傳應(yīng)用的是Gmail,它支持標(biāo)準(zhǔn)瀏覽器下拖拽本地文件到瀏覽器中作為郵件的附件發(fā)送。人人網(wǎng)的這個(gè)拖拽上傳也是同理,可以讓使用標(biāo)準(zhǔn)瀏覽器的用戶通過簡單的拖拽行為,將本地文件夾中的照片直接上傳到人人網(wǎng),用戶體驗(yàn)?zāi)艿玫教嵘耐瑫r(shí),也希望借此機(jī)會(huì)推廣一下標(biāo)準(zhǔn)瀏覽器,淘汰IE。人人網(wǎng)當(dāng)時(shí)也向廣大用戶推出升級(jí)瀏覽器活動(dòng),并喊出口號(hào):”工欲善其計(jì)算機(jī),必先利其瀏覽器”。本次拖拽上傳的宣傳口號(hào):你敢”脫”,我就敢上傳…

人人網(wǎng) - 拖拽上傳

  言歸正題,首先看看效果,大家如果有人人網(wǎng)帳號(hào)的話可以在首頁試一試拖拽上傳功能,下面是演示視頻:

拖拽上傳應(yīng)用主要使用了以下HTML5技術(shù):

  • Drag&Drop : HTML5基于拖拽的事件機(jī)制.
  • File API :  可以很方便的讓W(xué)eb應(yīng)用訪問文件對(duì)象,F(xiàn)ile API包括FileListBlobFileFileReaderURI scheme,本文主要講解拖拽上傳中用到的FileList和FileReader接口。
  • FormData : FormData是基于XMLHttpRequest Level 2的新接口,可以方便web應(yīng)用模擬Form表單數(shù)據(jù),最重要的是它支持文件的二進(jìn)制流數(shù)據(jù),這樣我們就能夠通過它來實(shí)現(xiàn)AJAX向后端發(fā)送文件數(shù)據(jù)了。

HTML5 Drag&Drop 拖拽事件

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

var oDragWrap = document.body;

 

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

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

//拖來拖去 , 一定要注意dragover事件一定要清除默認(rèn)事件
//不然會(huì)無法觸發(fā)后面的drop事件
oDragWrap.addEventListener(’dragover’, function(e) {
 e.preventDefault();
}, false);

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

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

獲取文件數(shù)據(jù) HTML5 File API

  在之前那篇文章中我也有介紹過關(guān)于File API中的FileReader接口,作為 File API 的一部分,FileReader 專門用于讀取文件,根據(jù) W3C 的定義,F(xiàn)ileReader 接口 “提供一些讀取文件的方法與一個(gè)包含讀取結(jié)果的事件模型”。關(guān)于FileReader的詳細(xì)介紹和代碼實(shí)例大家可以先去看看那篇文章

  今天我著重介紹一下File API中的FileList接口,它主要通過兩個(gè)途徑獲取本地文件列表,一是<input type=”file”>的表單形式,另一種則是e.dataTransfer.files拖拽事件傳遞的文件信息。很顯然,我們這里會(huì)用到后者。

var fileList = e.dataTransfer.files;

  使用files方法將會(huì)獲取到拖拽文件的數(shù)組形勢的數(shù)據(jù),每個(gè)文件占用一個(gè)數(shù)組的索引,如果該索引不存在文件數(shù)據(jù),將返回null值。可以通過length屬性獲取文件數(shù)量.

var fileNum = fileList.length;

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

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

  下面讓我們來看看如何結(jié)合之前的拖拽事件來實(shí)現(xiàn)拖拽圖片并在頁面中進(jìn)行預(yù)覽:

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

 

 //獲取文件列表
 var fileList = e.dataTransfer.files;

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

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

 //實(shí)例化file reader對(duì)象
 var reader = new FileReader();
 var img = document.createElement(’img’);

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

}

  這里有一個(gè)簡單的拖拽圖片預(yù)覽的Demo

  這時(shí)你如果用FireBug等類似調(diào)試工具查看DOM的話,會(huì)看到<img>標(biāo)簽的src屬性是一個(gè)超長的文件二進(jìn)制數(shù)據(jù),所以如果DOM有很多這類圖片,那就要當(dāng)心瀏覽器性能了,因?yàn)檫@些數(shù)據(jù)極大地?cái)U(kuò)充的頁面的代碼量,而每次頁面的reflow都會(huì)對(duì)瀏覽器形成很大的負(fù)擔(dān),So,如果這些圖片還在DOM中,那就盡量不要做動(dòng)畫或任何重繪操作,如果真的要做就盡量讓圖片脫離文檔流,讓其絕對(duì)定位比較靠譜。

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

readasdataurl-vs-createobjecturl

優(yōu)化后的代碼:(紅色為優(yōu)化的代碼)

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 {
  //實(shí)例化file reader對(duì)象
  var reader = new FileReader();

 

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

}

  需要注意的是,window.URL.createObjectURL是有生命周期的,也就意味著你每用此方法獲取URL,其生命周期都會(huì)和DOM一樣,它會(huì)單獨(dú)占用內(nèi)存,所以當(dāng)刪除圖片或不再需要它是,記得用window.URL.revokeObjectURL(file)來釋放其內(nèi)存。當(dāng)然,如果你沒有釋放,刷新頁面也是可以釋放的。

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

  既然已經(jīng)獲取到了拖拽到web頁面中圖片的數(shù)據(jù),下一步就是將其發(fā)送到服務(wù)器端了。

  話說HTML5時(shí)代之前,AJAX傳輸文件二進(jìn)制流數(shù)據(jù)是不可能完成的事情,而現(xiàn)在我們完全可以通過file.getAsBinary獲取文件的二進(jìn)制數(shù)據(jù)流,進(jìn)而將其當(dāng)做XHR的data數(shù)據(jù)傳送到后端,8過由于Chrome不支持file的getAsBinary方法,F(xiàn)F3.6+支持此方法。所以Chrome就要另尋它法了,這時(shí)我們發(fā)現(xiàn)XMLHttpRequest Level 2中的FormData接口完美解決了這個(gè)問題,它可以很快捷的模擬Form表單數(shù)據(jù)并通過AJAX發(fā)送至后端,F(xiàn)ormData的支持情況是FF5及以上支持,Chrome12及以上支持。

   file.getAsBinary獲取文件流很簡單,但是要想上傳數(shù)據(jù),就要模擬一下表單的數(shù)據(jù)格式了,首先看看模擬表單的js代碼, FormData模擬表單數(shù)據(jù)時(shí)更是簡潔,不用麻煩的去拼字符串,而是直接將數(shù)據(jù)append到formdata對(duì)象中即可:

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);

首先表單數(shù)據(jù)headers頭信息需要以下兩項(xiàng):

  • Content-Type : 設(shè)置其為multipart/form-data來模擬表單數(shù)據(jù)
  • boundary : 表單數(shù)據(jù)中的分隔符,用于分隔不同的文件或表單項(xiàng),這是服務(wù)器端設(shè)置的格式。

發(fā)送時(shí)的post數(shù)據(jù)類似這樣:

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

 

ÿØÿà?JFIF?…這里是文件二進(jìn)制流…~iúoî­5P%-vãîHü 4QHgÿÙ
————————-1323611763556
Content-Disposition: form-data; name=”hostid”

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

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

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

好了,現(xiàn)在文件上傳成功后你就可以按照平常AJAX的操作來進(jìn)行后續(xù)處理了。

最后,再來總結(jié)一下拖拽上傳的技術(shù)要點(diǎn):

  1. 監(jiān)聽拖拽:監(jiān)聽頁面元素的拖拽事件,包括:dragenter、dragover、dragleave和drop,一定要將dragover的默認(rèn)事件取消掉,不然無法觸發(fā)drop事件。如需拖拽頁面里的元素,需要給其添加屬性draggable=”true”;
  2. 獲取拖拽文件:在drop事件觸發(fā)后通過e.dataTransfer.files獲取拖拽文件列表,.length屬性獲取文件數(shù)量,.type屬性獲取文件類型。
  3. 讀取圖片數(shù)據(jù)并添加預(yù)覽圖:實(shí)例化FileReader對(duì)象,通過其readAsDataURL(file)方法獲取文件二進(jìn)制流,并監(jiān)聽其onload事件,將e.result賦值給img的src屬性,最后將圖片append到DOM中。
  4. 發(fā)送圖片數(shù)據(jù):使用file.getAsBinary 和 FormData分別模擬表單數(shù)據(jù)AJAX提交文件流。

OK,拖拽上傳就講到這里,歡迎大家一起探討。