说说我实习前端开发的时候用的大文件上传,前端原本项目用的是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="width: 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 + "\n");
//因为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>