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

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

文件上傳方式

文件上傳最原始的方式form元素表單提交,發(fā)展后form原始+iframe實現(xiàn)異步文件上傳,到后來HTML5出現(xiàn)ajax實現(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ù)進行編碼,在使用包含文件上傳控件的表單時必須使用“multipart/form-data”。


form表單提交

iframe封裝form表單

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

實現(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é)果實現(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)點就是我們可以異步上傳一個二進制文件。

構(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)在很多上傳功能都包含拖拽上傳,實現(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屬性表示被拖動到瀏覽器窗口中的文件列表。

文件上傳進度

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)即可得到上傳比例,可以用這個已上傳比例去更新進度條啦    
}
};
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);

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

文件MD5

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

File API

要在瀏覽器中對文件進行md5,基本思路就是使用HTML5的FileReader接口把文件讀取到內(nèi)存,然后獲取文件的二進制內(nèi)容,最后再進行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ù)覽首先要判斷文件類型是否為圖片類型,可以通過正則表達式匹配判斷

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碼
文件上傳的漸進式增強
在web應(yīng)用中使用文件
拖放操作
在瀏覽器端獲取文件的MD5值
js-spark-md5

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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