上傳文件已經(jīng)是個已經(jīng)成熟的前端技術(shù),目前開源的拿來即用的前端上傳插件也比較多,諸如:Web Uploader、JSAjaxFIleUploader捧毛、
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”屹篓。
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操作文件成為可能役首;
要在瀏覽器中對文件進(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