说说我实习前端开发的时候用的大文件上传,前端原本项目用的是element自带的el-upload文件上传,确实很方便,element把数据上传成功,失败,上传中等等的监听事件都已经封装好了,文件列表和文件信息也携带在监听方法的参数中,调用然后打印,,一目了然,进行业务逻辑开发效率很高。但问题是,,element的upload没有附带大文件的断点续传功能,上传过程中如果中断那么就比较麻烦,所以需要自己开发。
什么是断点续传?
当用户上传文件过程中如果由于网络,,手滑,,或者其他骚操作等种种原因突然中断上传,那原本上传一半的文件要怎么处理???下次上传这个文件还得全部重来??这样是很浪费性能和资源的。并且,http协议和Springboot都限制了文件的上传大小,文件太大怎么办??这个时候,大文件的断点续传技术完美解决。
它将一个文件切割成若干部分分开向服务器端上传,每个小的部分我们称为切块,每上传结束一个切块除了保存文件信息,还会在后端保存切块的“”识别码”,用于识别文件上传到哪儿了,等到下次上传时,直接从这个位置开始继续上传,这样大大节省了开销。而且还有一个亮点,如果文件已经上传过,那么可以对后台进行秒传,节省大量时间,用户体验也大大提高。
想要实现大文件断点续传,我们只需要安装一个插件WebUploader,然后在前端js代码中触发监听,配置相关的变量就可以实现断点续传了,灵活性很高。。。
插件的底层源码是用JQuery封装的,所以需要安装JQuery
如果是vue项目,npm安装到环境中:
npm install JQuery
npm install WebUploader
然后在vue页面中引入:
import $ from Jquery
import webUploader from WebUploader
然后就可以在项目中触发对应的方法和配置了。。
WebUploader分为三个部分:
1.注册三个事件,文件上传前,分片上传前和分片上传后,创建WebUploader实例对象,配置文件块大小,上传地址,文件限制大小等变量。。
2.先判断是否上传过该文件,调用接口。如果上传过,进行秒传,没有则进行切块。
3.切块后进行分片上传,获取分片的编号,确认分片,
4.等全部分片上传完成后向后端请求合并分块,成一个完整的文件。
可以在监听方法中写自己想要的功能代码。。。并且在上传中还包含了进度条信息可以看进度。。
下面贴上完整前端代码,自行修改请求路径,也可以修改文件的上传配置,可以修改样式,。。。
(script文件需要引入)
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="webuploader.min.js"></script> <link href="webuploader.css" type="css/text"/> <style type="text/css"> .test-bar { height: 100%; background-color: #0b0e9e; } input{ height: 0px; } </style> </head> <body> <div class="uploadWrapper"> <div class="btnUpload"> <div id="picker" class="form-control-focus">点击选择文件上传</div> </div> <div id="thelist" class="uploader-list"> </div> <button id="btnSync" type="button" class="btn btn-warning">开始同步</button> </div> <script> //项目ip function getRootPath(){ return("http://localhost:53021"); } getRootPath(); $("#btnSync").hide(); //该map,用于给uploader.options.formData表达赋一个动态的键值对. var map = {}; // var testMd5; //定义文件的分片大小 var chunkSize = 0.9 * 1024 * 1024; //监听分块上存过程中的三个时间点 WebUploader.Uploader.register({ "before-send-file": "beforeSendFile",//整个文件上存前,触发方法beforeSendFile "before-send": "beforeSend",//每个分片上存前,触发方法beforeSend "after-send-file": "afterSendFile",//分片上存完毕后,触发方法afterSendFile }, { //时间点1:所有分块进行上存之前触发该方法,即当每个文件开始上传第一个分块前就调用该方法 beforeSendFile: function (file) { console.log("执行时间点1的方法。。。。。"); //定义一个异步对象 var deferred = WebUploader.Deferred(); if (!map[file.id]) { deferred.reject(); alert("文件解析出错,请刷新后重写上传."); return deferred.promise(); } //显示暂停按键并且隐藏删除按键 switchButton(file.id); //先查看服务器中是否已经有该文件 $.ajax({ type: "POST", dataType: "json", url: getRootPath()+"/resource/secondPass", // timeout: 3000, data: { "fileSize": file.size, "fileName": file.name, "md5Val": map[file.id] }, success: function (data) { if (data.status === "1") { console.log("............................................输出返回值:" + data.status); deferred.reject(); uploader.skipFile(file); //清除进度条,如果是秒传,那么是不会触发方法uploader.on('uploadComplete' fadeOutProgress(file); $('#' + file.id).find("p.state").text("已经上传"); deferred.resolve(); } else { $('#' + file.id).find("p.state").text("正在上传..."); deferred.resolve(); } }, error: function (data) { deferred.reject(); console.log(JSON.stringify(data)); $('#' + file.id).find("p.state").text("上传出错..."); alert("网络错误,请刷新后再上传"); } }); return deferred.promise(); }, //时间点2:如果有分块上传,则每个分块上传之前调用此函数 //用于文件的续传 beforeSend: function (block) { console.log("执行时间点2的方法。。。"); var deferred = WebUploader.Deferred(); $.ajax({ type: "POST", dataType: "json", url: getRootPath()+"/resource/checkChunk", data: { "chunk": block.chunk, //block.file.id,获取该分片对应的文件的id,从而获取该文件的md5值 "md5Val": map[block.file.id] }, success: function (data) { if (data.status === "1") { console.log("跳过.."); //分片存在,跳过 deferred.reject(); } else { console.log("上传.."); //分片不存在,那么就上传. deferred.resolve(); } }, error: function (data) { console.log("时间点2出错:" + JSON.stringify(data)); //如果是一般的请求出错,那么也可以尝试上传 deferred.resolve(); } }); return deferred.promise(); }, //时间点3:一个文件的所有分片上传成功后,调用该方法,让后台合并所有分片 //该方法的在uploader.on("success")方法前执行。 afterSendFile: function (file) { $('#' + file.id).find('p.state').text("后台正在合并文件..."); //上传成功后,异步请求后台的servlet,发送的数据有guid(该文件所有分片保存的目录),chunks(该文件一共分了多少片,注意要向上取整),filename(文件名) $.ajax({ type: "POST", dataType: "json", url: getRootPath()+"/resource/mergeChunk", data: { "guid": uploader.options.formData.guid, "fileSize": file.size, "chunks": Math.ceil(file.size / chunkSize), "fileName": file.name, "md5Val": map[file.id] }, success: function (data) { console.log(JSON.stringify(data)) if (data.status === "success") { $('#' + file.id).find('p.state').text('已经上传'); } else { $('#' + file.id).find('p.state').text('上传失败'); } }, error: function (data) { alert(JSON.stringify(data)); $('#' + file.id).find("p.state").text("上传出错..."); } }); console.log("执行时间点3的方法。。。"); } }); var uploader = WebUploader.create({ // swf文件路径 swf: 'webuploader/Uploader.swf', // 文件接收服务端。 server: getRootPath()+'/resource/upload', // 选择文件的按钮。可选。 // 内部根据当前运行是创建,可能是input元素,也可能是flash. pick: '#picker', compress: null,//图片不压缩 chunked: true, //分片处理 chunkSize: chunkSize, //每片5M chunkRetry: 3,//由于网络原因出现的故障,最多允许分片自动重转3次 threads: 8,//上传并发数。允许同时最大上传进程数。 fileSizeLimit: 12 * 1024 * 1024 * 1024,//12G 验证文件总大小是否超出限制, 超出则不允许加入队列 fileSingleSizeLimit: 5 * 1024 * 1024 * 1024, //5G 验证单个文件大小是否超出限制, 超出则不允许加入队列 fileNumLimit: 100, formData: { test: 123 }, //禁用全局拖拽功能 disableGlobalDnd: true, // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传! resize: false }); console.log(uploader.upload()); // 当有文件被添加进队列的时候触发 uploader.on('fileQueued', function (file) { alert("123"); console.log(file); $("#thelist").append( '<div id="' + file.id + '" class="item">' + '<h4 class="info">' + file.name + '</h4>' + '<p class="state">等待上传...</p>' + '<button class="btn btn-info btn-stop" style="display:none;">' + '暂停' + '</button>' + '<a href="javascript:void(0);" class="btn btn-primary file_btn btnRemoveFile" >' + '删除' + '</a>' + '<div class="progress">' + '<div id="' + file.id + 'progress" class="test-bar" style=" 0%;" >' + "<span>" + "</span>" + '</div>' + '</div>' + '</div>'); (new WebUploader.Uploader()).md5File(file, 0, 10 * 1024 * 1024).progress(function (percentage) { $('#' + file.id).find('p.state').text("正在读取文件信息..." + parseInt(percentage * 100) + "%"); }) //当文件读取完后,就执行then方法 .then(function (md5Val) { map[file.id] = md5Val; $('#' + file.id).find("p.state").text("成功读取文件信息"); //uploader.options.formData.file.id=md5Val,这种方式中"file.id"只能作为一个字符串 //下面种方式可以给uploader.options.formData表单动态赋一个键值对 $.extend(uploader.options.formData, map); }); $("#btnSync").show(); var obj = 'div[id=' + file.id + ']'; console.log("打印id啊:" + $(obj).attr("id")); //给“删除”按键绑定监听事件 $('#' + file.id + " a").bind("click", function () { var fileItem = $(this).parent(); var fileId = $(fileItem).attr("id"); console.log("删除",map); if (!map[fileId]) { alert("正在解析文件,请稍后再操作...") return; } // console.log("输出id:" +fileId); //$(fileItem).attr("id")意思是,获取到fileItem该标签的id属性的值,true为从队列中移除 uploader.removeFile(file, true); //同时取消文件上传 uploader.cancelFile(file); delete map[fileId]; var len = $('#thelist').children("div").length; console.log("删除前的文件列表长度:" + len); //渐变的效果的消失 $(fileItem).fadeOut(function () { $(fileItem).remove(); }); //由于上面的remove方法是异步删除,因此当下面获取长度的时候,长度还是不变,因此当长度为1的时候,其实list中就没有文件了 var len = $('#thelist').children("div").length; console.log("打印文件列表长度:" + len); if (len === 0||len===1) { $("#btnSync").hide(); } }); //给“暂停”按键绑定监听事件 $('#' + file.id + " button").bind("click", function () { console.log("暂停"); clickStopButton(file); }); }); //上传过程中,一直会执行该方法 uploader.on('uploadProgress', function (file, percentage) { console.log("当前文件" + file.id + "上传的百分比" + percentage + " "); //因为percentage是百分比(小数来的),因此要显示进度条效果,就先乘100,然后(percentage*100)%作为进度条的宽度百分比, // 就可以实现进度条效果 // $('#' + file.id).children($("#test-bar")).css("width", parseInt(percentage * 100) + "%"); $('#' + file.id + 'progress').css("width", parseInt(percentage * 100) + "%"); }); /** * 文件上传成功后,就在该文件对应的位置上,显示上传成功,file.id,作为上传文件位置标签的id, */ uploader.on('uploadSuccess', function (file) { switchButton(file.id); console.log("执行上传成功的方法。。"); // $('#' + file.id).find("p.state").text("上传成功。。。"); }); uploader.on('uploadError', function (file) { switchButton(file.id); $('#' + file.id).find('p.state').text('上传出错...'); }); //不管所有分片发送成功或者失败都会执行该方法 uploader.on('uploadComplete', function (file) { console.log("执行上传完成的方法"); // //上传完成就删除进度条 fadeOutProgress(file); }); /** * 当点击上传文件的时候,就触发该方法 */ $("#btnSync").on('click', function () { //获取文件列表的长度 var len = $('#thelist').children("div").length; //获取计算出md5的文件数 var mapSize = Object.keys(map).length; //一定要全部文件都计算出md5的值,才能上存 if (len !== mapSize) { alert("文件正在解析,请稍等.."); return; } console.log(uploader.upload()); uploader.upload(); }); //该方法删除指定文件下的进度条 function fadeOutProgress(file) { $('#' + file.id).find('.progress').fadeOut(); } //指定文件下的,“暂停”键,和“删除”按键切换显示状态, function switchButton(fileId) { var display= $('#' + fileId + " button").css('display'); if(display==='none') { //暂停键显示 $('#' + fileId + " button").css("display", ""); //删除键隐藏 $('#' + fileId + " a").css("display", "none"); }else { //暂停键隐藏 $('#' + fileId + " button").css("display", "none"); //删除键显示 $('#' + fileId + " a").css("display", ""); } } //指定文件下,点击了“暂停”,则按键变为“继续”,反之一样 function clickStopButton(file) { var content=$('#' + file.id + " button").text(); if(content.trim()==="暂停"){ //暂停上传 uploader.stop(true); console.log("暂停"); $('#' + file.id + " button").text("继续").addClass("btn-warning"); //删除键显示 $('#' + file.id + " a").css("display", ""); }else if(content.trim()==="继续"){ console.log("继续"); //继续上传 uploader.upload(); $('#' + file.id + " button").text("暂停").removeClass("btn-warning"); //删除键隐藏 $('#' + file.id + " a").css("display", "none"); } } // function testInterval() { // var a = 0; // var flag = setInterval(function () { // a = a + 1; // if (a === 5) { // clearInterval(flag); // } // console.log("计时器。。。" + a) // }, 1000); // // // } </script> </body> </html>