前端文件上传基础

上传文件已经是个已经成熟的前端技术,目前开源的拿来即用的前端上传插件也比较多,诸如:Web UploaderJSAjaxFIleUploader
jQuery-File-Upload,通常这些上传插件包含的功能有:选择上传、支持拖拽、MD5校验、图片预览、上传进度显示等功能;
这篇文章主要分析讨论前端上传控件的功能实现原理,以及上传功能如何做到功能的渐进式增强。

文件上传方式

文件上传最原始的方式form元素表单提交,发展后form原始+iframe实现异步文件上传,到后来HTML5出现ajax实现文件上传。所以通常上传控件向下兼容的方案通常是高版本浏览器采用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属性规定后端处理文件上传的路径;method属性规定上传文件的方法post or get;enctype属性规定在发送到服务器之前应该如何对表单数据进行编码,在使用包含文件上传控件的表单时必须使用“multipart/form-data”。


form表单提交

iframe封装form表单

使用form元素比较简单,但缺点也比较明显:上传同步、上传完成页面会刷新;
在HTML5出现之前,想要实现文件异步上传,只能通过iframe+form实现;

实现方式

原理:文件上传时在页面中动态创建一个iframe元素和一个form元素,并将form元素的target属性指向动态创建iframe元素。当用户完成选择文件动作时,提交子页面中的 form。这时,iframe跳转,而父页面没有刷新。这使得上传结束后,服务器处理结果返回到动态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();
})

上述程序实现了,id值为“j-upload-input”的input元素,在触发文件选择时(onchange事件),动态创建一个form元素和一个iframe元素,input加入一个动态创建form元素,并将form元素的target值指向iframe元素,最终结果实现了触发input文件选择,发送文件请求,但是页面不刷新;


文件上传不刷新

结果处理

通过iframe+form上传,上传结果处理需要前后端配合;
1.前后端预先约定好回调函数名;
例如,在当前页面中定义好上传的回调函数。
function uploadCallBack (resp){...}

服务返回的数据形式可以为:

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

通过window.top.window[uploadCallBack]可以调用到iframe父级元素中定义的uploadCallBack方法,也就是预先定义的回调处理;
2.前端页可以监听frame 的onLoad确定是否请求超时和后端是否给予返回;

通过FormData ajax方式

XMLHttpRequest Level 2添加了一个新的接口FormData利用FormData对象,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()
方法来异步的提交这个"表单"。比起普通的ajax,使用FormData
的最大优点就是我们可以异步上传一个二进制文件。

构建一个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);

通过拖拽操作选择文件

现在很多上传功能都包含拖拽上传,实现上传功能首先要创建一个拖放操作的目的区域并应用程序的设计来决定哪部分的内容接受 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”的元素为文件拖拽上传受理区域,我们需要在该元素上绑定 dragover,dragleave,和drop 事件。
其中dragover,当拖拽中的鼠标移动经过一个元素的时候触发,可以做一些文件经过,拖拽区域高亮处理。dragleave当拖拽中的鼠标离开元素时触发。监听器需要将作为可释放反馈的高亮或插入标记去除。drop
这个事件在拖拽操作结束释放时于释放元素上触发。一个监听器用来响应接收被拖拽的数据并插入到释放之地。

function dragoverHandler(event) {    
event.stopPropagation();   
 event.preventDefault();    
......
//这里可以添加拖拽区域背景高亮处理样式
}
function dragleaveHandler(event) {    
event.stopPropagation();    
event.preventDefault();    
......
//这里可以异常拖拽区域背景高亮处理的样式
}
function dropHandler(event) {   
 event.stopPropagation();   
 event.preventDefault();    
//获取并处理文件
var dt = event.dataTransfer; 
var files = dt.files; 
handleFiles(files);
}

在代码中的event.dataTransfer.files属性表示被拖动到浏览器窗口中的文件列表。

文件上传进度

XMLHttpRequest Level 2中,传送数据的时候,有一个progress事件,上传数据progress事件属于XMLHttpRequest.upload对象,上传数据过程中会触发。事件回调函数中可以使用事件event的下列属性:event.total是需要传输的总字节;event.loaded是已经传输的字节;如果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是需要传输的总字节,e.loaded是已经传输的字节。但如果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接口把文件读取到内存,然后获取文件的二进制内容,最后再进行md5。
读取文件

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

文件切割

//file的slice方法,注意它的兼容性,在不同浏览器的写法不同
blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice
//然后指定file和开始结束的片段,就可以得到切割的文件了。
blobSlice.call(file, start, end);

计算文件MD5

spark = new SparkMD5();
spark.appendBinary(filepice1);
spark.appendBinary(filepice2);
spark.appendBinary(filepice3);
....//所有的分片处理好之后调用下面的方法就能获取到文件的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();
});

图片预览

如果上传的文件是图片类型,上传插件通常会提供图片预览功能,图片预览首先要判断文件类型是否为图片类型,可以通过正则表达式匹配判断

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

读取和显示图片,首先要构建一个img元素标签,给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应用中使用文件
拖放操作
在浏览器端获取文件的MD5值
js-spark-md5

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容