
早在公元2011年6月3日傍晚,人人網(wǎng)推出了一個(gè)很裝B且完全無視IE瀏覽器的功能——拖拽上床。哦,Sorry, 是拖拽上傳。本文將重點(diǎn)介紹實(shí)現(xiàn)拖拽上傳的幾個(gè)HTML5技術(shù):Drag&Drop、FileReader API和FormData。
關(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)帳號(hào)的話可以在首頁試一試拖拽上傳功能,下面是演示視頻:
拖拽上傳應(yīng)用主要使用了以下HTML5技術(shù):
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ì)比:

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