
早在公元2011年6月3日傍晚,人人網推出了一個很裝B且完全無視IE瀏覽器的功能——拖拽上床。哦,Sorry, 是拖拽上傳。本文將重點介紹實現拖拽上傳的幾個HTML5技術:Drag&Drop、FileReader API和FormData。
關于這個拖拽上傳,其實國外有很多網站已經有這樣的應用,最早推出拖拽上傳應用的是Gmail,它支持標準瀏覽器下拖拽本地文件到瀏覽器中作為郵件的附件發送。人人網的這個拖拽上傳也是同理,可以讓使用標準瀏覽器的用戶通過簡單的拖拽行為,將本地文件夾中的照片直接上傳到人人網,用戶體驗能得到提升的同時,也希望借此機會推廣一下標準瀏覽器,淘汰IE。人人網當時也向廣大用戶推出升級瀏覽器活動,并喊出口號:”工欲善其計算機,必先利其瀏覽器”。本次拖拽上傳的宣傳口號:你敢”脫”,我就敢上傳…

言歸正題,首先看看效果,大家如果有人人網帳號的話可以在首頁試一試拖拽上傳功能,下面是演示視頻:
拖拽上傳應用主要使用了以下HTML5技術:
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]);
}
這里有一個簡單的拖拽圖片預覽的Demo
這時你如果用FireBug等類似調試工具查看DOM的話,會看到<img>標簽的src屬性是一個超長的文件二進制數據,所以如果DOM有很多這類圖片,那就要當心瀏覽器性能了,因為這些數據極大地擴充的頁面的代碼量,而每次頁面的reflow都會對瀏覽器形成很大的負擔,So,如果這些圖片還在DOM中,那就盡量不要做動畫或任何重繪操作,如果真的要做就盡量讓圖片脫離文檔流,讓其絕對定位比較靠譜。
補充:可以使用window.URL.createObjectURL(file)來獲取文件的URL(Chrome下用window.webkitURL.createObjectURL(file)),這種方式獲取的URL要比上面說的readAsDataURL簡短很多。而且可以省去使用FileReader。這里感謝BinBinLiao的留言建議:) 下面是使用readAsDataURL與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的操作來進行后續處理了。
最后,再來總結一下拖拽上傳的技術要點:
- 監聽拖拽:監聽頁面元素的拖拽事件,包括:dragenter、dragover、dragleave和drop,一定要將dragover的默認事件取消掉,不然無法觸發drop事件。如需拖拽頁面里的元素,需要給其添加屬性draggable=”true”;
- 獲取拖拽文件:在drop事件觸發后通過e.dataTransfer.files獲取拖拽文件列表,.length屬性獲取文件數量,.type屬性獲取文件類型。
- 讀取圖片數據并添加預覽圖:實例化FileReader對象,通過其readAsDataURL(file)方法獲取文件二進制流,并監聽其onload事件,將e.result賦值給img的src屬性,最后將圖片append到DOM中。
- 發送圖片數據:使用file.getAsBinary 和 FormData分別模擬表單數據AJAX提交文件流。
OK,拖拽上傳就講到這里,歡迎大家一起探討。