前端文件上傳基礎(chǔ)

上傳文件已經(jīng)是個已經(jīng)成熟的前端技術(shù),目前開源的拿來即用的前端上傳插件也比較多,諸如:Web UploaderJSAjaxFIleUploader捧毛、
jQuery-File-Upload欺旧,通常這些上傳插件包含的功能有:選擇上傳姑丑、支持拖拽、MD5校驗(yàn)辞友、圖片預(yù)覽栅哀、上傳進(jìn)度顯示等功能;
這篇文章主要分析討論前端上傳控件的功能實(shí)現(xiàn)原理称龙,以及上傳功能如何做到功能的漸進(jìn)式增強(qiáng)留拾。

文件上傳方式

文件上傳最原始的方式form元素表單提交,發(fā)展后form原始+iframe實(shí)現(xiàn)異步文件上傳鲫尊,到后來HTML5出現(xiàn)ajax實(shí)現(xiàn)文件上傳痴柔。所以通常上傳控件向下兼容的方案通常是高版本瀏覽器采用ajax方式,低版本瀏覽器采用iframe+form表單形式疫向。

form表單提交

<form id="j-puload-form" action="/fileUpload" method="post" enctype="multipart/form-data">    
    <input type="file" id="j-upload-input" name="upload"/><button type="submit">提交</button>
</form>

form表單屬性中action屬性規(guī)定后端處理文件上傳的路徑咳蔚;method屬性規(guī)定上傳文件的方法post or get;enctype屬性規(guī)定在發(fā)送到服務(wù)器之前應(yīng)該如何對表單數(shù)據(jù)進(jìn)行編碼鸿捧,在使用包含文件上傳控件的表單時必須使用“multipart/form-data”屹篓。


form表單提交

iframe封裝form表單

使用form元素比較簡單,但缺點(diǎn)也比較明顯:上傳同步匙奴、上傳完成頁面會刷新堆巧;
在HTML5出現(xiàn)之前,想要實(shí)現(xiàn)文件異步上傳泼菌,只能通過iframe+form實(shí)現(xiàn);

實(shí)現(xiàn)方式

原理:文件上傳時在頁面中動態(tài)創(chuàng)建一個iframe元素和一個form元素谍肤,并將form元素的target屬性指向動態(tài)創(chuàng)建iframe元素。當(dāng)用戶完成選擇文件動作時哗伯,提交子頁面中的 form荒揣。這時,iframe跳轉(zhuǎn)焊刹,而父頁面沒有刷新系任。這使得上傳結(jié)束后,服務(wù)器處理結(jié)果返回到動態(tài)iframe窗口而沒有刷新頁面虐块;

<input type="file" id="j-upload-input" name="upload"/>
var createUploadForm = function (id, fileElementId) {  
      //create form    
    var formId = 'jUploadForm' + id;    
    var fileId = 'jUploadFile' + id;
    var form = $('<form  action="" method="POST" name="' + formId + '" id="' + formId + '" enctype="multipart/form-data"></form>');    
    var oldElement = $('#' + fileElementId);    
    var newElement = $(oldElement).clone();    
    $(oldElement).attr('id', fileId);     
    $(oldElement).before(newElement);     
    $(oldElement).appendTo(form);    
    $(form).css('position', 'absolute');    
    $(form).css('top', '-1200px');    
    $(form).css('left', '-1200px');    
    $(form).appendTo('body');    
    return form;
}
var createUploadIframe = function (id) {    
//create frame    
var frameId = 'jUploadFrame' + id;    
var iframeHtml = '<iframe id="' + frameId + '" name="' + frameId + '" style="position:absolute; top:-9999px; left:-9999px"' + ' src="' + '" />';    
$(iframeHtml).appendTo(document.body);    
return jQuery('#' + frameId).get(0);
}
var actionURL = "/fileUpload";
$('#j-upload-input').change(function () {    
    var id = new Date().getTime() ;   
    var frameId = 'jUploadFrame' + id;    
    var formId = 'jUploadForm' + id;    
    var form = createUploadForm(id, "j-upload-input");
    var frame = createUploadIframe(id);   
    form.appendTo(document.body);   
    var form = $('#' + formId);    
    $(form).attr('action', actionURL);   
    $(form).attr('method', 'POST');    
    $(form).attr('target', frameId);    
    $(form).attr('enctype', 'multipart/form-data');    
    $(form).submit();
})

上述程序?qū)崿F(xiàn)了俩滥,id值為“j-upload-input”的input元素,在觸發(fā)文件選擇時(onchange事件)贺奠,動態(tài)創(chuàng)建一個form元素和一個iframe元素霜旧,input加入一個動態(tài)創(chuàng)建form元素,并將form元素的target值指向iframe元素儡率,最終結(jié)果實(shí)現(xiàn)了觸發(fā)input文件選擇挂据,發(fā)送文件請求以清,但是頁面不刷新;


文件上傳不刷新

結(jié)果處理

通過iframe+form上傳崎逃,上傳結(jié)果處理需要前后端配合;
1.前后端預(yù)先約定好回調(diào)函數(shù)名掷倔;
例如,在當(dāng)前頁面中定義好上傳的回調(diào)函數(shù)婚脱。
function uploadCallBack (resp){...}

服務(wù)返回的數(shù)據(jù)形式可以為:

 <script type="text/javascript">
    window.top.window['uploadCallBack'](resp);
  </script>

通過window.top.window[uploadCallBack]可以調(diào)用到iframe父級元素中定義的uploadCallBack方法今魔,也就是預(yù)先定義的回調(diào)處理;
2.前端頁可以監(jiān)聽frame 的onLoad確定是否請求超時和后端是否給予返回障贸;

通過FormData ajax方式

XMLHttpRequest Level 2添加了一個新的接口FormData利用FormData對象,我們可以通過JavaScript用一些鍵值對來模擬一系列表單控件吟宦,我們還可以使用XMLHttpRequest的send()
方法來異步的提交這個"表單"篮洁。比起普通的ajax,使用FormData
的最大優(yōu)點(diǎn)就是我們可以異步上傳一個二進(jìn)制文件殃姓。

構(gòu)建一個FormData并上傳文件

var xhr = new XMLHttpRequest();
var formData = new FormData();
for (var key in params) {    
    formData.append(key, params[key]);
}
formData.append(fileName, fileObj);
xhr.open(this.options.method, this.options.url, true);
xhr.send(formData);

通過拖拽操作選擇文件

現(xiàn)在很多上傳功能都包含拖拽上傳袁波,實(shí)現(xiàn)上傳功能首先要創(chuàng)建一個拖放操作的目的區(qū)域并應(yīng)用程序的設(shè)計來決定哪部分的內(nèi)容接受 drop;

var dragArea;
if ((dragArea = document.getElementById("j-drag-area")) && dragArea.addEventListener) {    
    dragArea.addEventListener("dragover", dragoverHandler, false);    
    dragArea.addEventListener("dragleave", dragleaveHandler, false);    
    dragArea.addEventListener("drop", dropHandler, false);}

在例子中定義了id值為“j-drag-area”的元素為文件拖拽上傳受理區(qū)域蜗侈,我們需要在該元素上綁定 dragover篷牌,dragleave,和drop 事件踏幻。
其中dragover枷颊,當(dāng)拖拽中的鼠標(biāo)移動經(jīng)過一個元素的時候觸發(fā),可以做一些文件經(jīng)過该面,拖拽區(qū)域高亮處理夭苗。dragleave當(dāng)拖拽中的鼠標(biāo)離開元素時觸發(fā)。監(jiān)聽器需要將作為可釋放反饋的高亮或插入標(biāo)記去除隔缀。drop
這個事件在拖拽操作結(jié)束釋放時于釋放元素上觸發(fā)题造。一個監(jiān)聽器用來響應(yīng)接收被拖拽的數(shù)據(jù)并插入到釋放之地。

function dragoverHandler(event) {    
event.stopPropagation();   
 event.preventDefault();    
......
//這里可以添加拖拽區(qū)域背景高亮處理樣式
}
function dragleaveHandler(event) {    
event.stopPropagation();    
event.preventDefault();    
......
//這里可以異常拖拽區(qū)域背景高亮處理的樣式
}
function dropHandler(event) {   
 event.stopPropagation();   
 event.preventDefault();    
//獲取并處理文件
var dt = event.dataTransfer; 
var files = dt.files; 
handleFiles(files);
}

在代碼中的event.dataTransfer.files屬性表示被拖動到瀏覽器窗口中的文件列表猾瘸。

文件上傳進(jìn)度

XMLHttpRequest Level 2中界赔,傳送數(shù)據(jù)的時候,有一個progress事件牵触,上傳數(shù)據(jù)progress事件屬于XMLHttpRequest.upload對象淮悼,上傳數(shù)據(jù)過程中會觸發(fā)。事件回調(diào)函數(shù)中可以使用事件event的下列屬性:event.total是需要傳輸?shù)目傋止?jié)荒吏;event.loaded是已經(jīng)傳輸?shù)淖止?jié)敛惊;如果event.lengthComputable不為真,則event.total等于0绰更。

var xhr = new XMLHttpRequest(),        
formData = new FormData();
xhr.onreadystatechange = function () {    
if (xhr.readyState == 4) {// 4 = "loaded"        
onComplete(xhr);//上傳完成處理    }};
xhr.upload.onprogress = function (e) {    
if (e.lengthComputable) {        
onProgressHandler( e.loaded, e.total, xhr);        
//e.total是需要傳輸?shù)目傋止?jié)瞧挤,e.loaded是已經(jīng)傳輸?shù)淖止?jié)锡宋。但如果e.lengthComputable值為false,則e.total等于0特恬。       
// 通過(e.loaded/e.total)即可得到上傳比例执俩,可以用這個已上傳比例去更新進(jìn)度條啦    
}
};
xhr.open(this.options.method, this.options.url, true);
for (var key in params) {    
formData.append(key, params[key]);
}
formData.append(fileName, fileObj);
xhr.send(formData);

對于低版本瀏覽器則可以用通過輪詢的方式獲取上傳進(jìn)度;

文件MD5

HTML5 DOM新增的File API癌刽,使得JavaScript操作文件成為可能役首;

File API

要在瀏覽器中對文件進(jìn)行md5,基本思路就是使用HTML5的FileReader接口把文件讀取到內(nèi)存显拜,然后獲取文件的二進(jìn)制內(nèi)容衡奥,最后再進(jìn)行md5。
讀取文件

file = document.getElementById("file").files[0];

文件切割

//file的slice方法远荠,注意它的兼容性矮固,在不同瀏覽器的寫法不同
blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice
//然后指定file和開始結(jié)束的片段,就可以得到切割的文件了譬淳。
blobSlice.call(file, start, end);

計算文件MD5

spark = new SparkMD5();
spark.appendBinary(filepice1);
spark.appendBinary(filepice2);
spark.appendBinary(filepice3);
....//所有的分片處理好之后調(diào)用下面的方法就能獲取到文件的MD5了
spark.end()

附上js-spark-md5計算文件MD5方法 Demo源碼

document.getElementById('file').addEventListener('change',   function () { 
    var blobSlice = File.prototype.slice || File.prototype.mozSlice ||     File.prototype.webkitSlice, 
    file = this.files[0],
     chunkSize = 2097152, // Read in chunks of 2MB 
    chunks = Math.ceil(file.size / chunkSize),
     currentChunk = 0, 
    spark = new SparkMD5.ArrayBuffer(), 
    fileReader = new FileReader(); 
    fileReader.onload = function (e) { 
        console.log('read chunk nr', currentChunk + 1, 'of', chunks); 
        spark.append(e.target.result); // Append array buffer 
        currentChunk++;
         if (currentChunk < chunks) { 
            loadNext(); 
        } else {
             console.log('finished loading'); 
            console.info('computed hash', spark.end()); 
            // Compute hash
         } 
    }; 
    fileReader.onerror = function () { 
        console.warn('oops, something went wrong.');
     };
     function loadNext() {
         var start = currentChunk * chunkSize,
         end = ((start + chunkSize) >= file.size) ? file.size : start +    chunkSize;
         fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
     } 
    loadNext();
});

圖片預(yù)覽

如果上傳的文件是圖片類型档址,上傳插件通常會提供圖片預(yù)覽功能,圖片預(yù)覽首先要判斷文件類型是否為圖片類型邻梆,可以通過正則表達(dá)式匹配判斷

var imageType = /^image\//; 
if ( imageType.test(file.type) ) { 
    //是圖片;
 }

讀取和顯示圖片守伸,首先要構(gòu)建一個img元素標(biāo)簽,給img的src屬性賦值浦妄;讀取圖片文件可用new FileReader()對象的readAsDataURL(file)方法尼摹,方法返回文件的base64編碼串。
例子:
html

<input type="file" onchange="previewFile()"><br>
<img src="" height="200" alt="Image preview...">
function previewFile() { 
    var preview = document.querySelector('img'); 
    var file = document.querySelector('input[type=file]').files[0]; 
    var reader = new FileReader(); 
    reader.addEventListener("load", function () { 
        preview.src = reader.result; 
    }, false); 
    if (file) { 
    reader.readAsDataURL(file); 
    }
}

參考:

FormData
Using XMLHttpRequest
HTML5 file api 讀取文件MD5碼
文件上傳的漸進(jìn)式增強(qiáng)
在web應(yīng)用中使用文件
拖放操作
在瀏覽器端獲取文件的MD5值
js-spark-md5

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末校辩,一起剝皮案震驚了整個濱河市窘问,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宜咒,老刑警劉巖惠赫,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異故黑,居然都是意外死亡儿咱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門场晶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來混埠,“玉大人,你說我怎么就攤上這事诗轻∏埽” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吏颖。 經(jīng)常有香客問我搔体,道長,這世上最難降的妖魔是什么半醉? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任疚俱,我火速辦了婚禮,結(jié)果婚禮上缩多,老公的妹妹穿的比我還像新娘呆奕。我一直安慰自己,他們只是感情好衬吆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布梁钾。 她就那樣靜靜地躺著,像睡著了一般逊抡。 火紅的嫁衣襯著肌膚如雪陈轿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天秦忿,我揣著相機(jī)與錄音,去河邊找鬼蛾娶。 笑死灯谣,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛔琅。 我是一名探鬼主播胎许,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼罗售!你這毒婦竟也來了辜窑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤寨躁,失蹤者是張志新(化名)和其女友劉穎穆碎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體职恳,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡所禀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了放钦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片色徘。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖操禀,靈堂內(nèi)的尸體忽然破棺而出褂策,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布斤寂,位于F島的核電站耿焊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扬蕊。R本人自食惡果不足惜搀别,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尾抑。 院中可真熱鬧歇父,春花似錦、人聲如沸再愈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翎冲。三九已至垂睬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抗悍,已是汗流浹背驹饺。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缴渊,地道東北人赏壹。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像衔沼,于是被迫代替她去往敵國和親蝌借。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容