大文件上传vue+WebUploader
说说我实习前端开发的时候用的大文件上传,前端原本项目用的是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.等全部分片上传完成后向后端请求合并分块,成一个完整的文件。
html效果图
前端代码示例
下面贴上完整前端代码,自行修改请求路径,也可以修改文件的上传配置,可以修改样式,。。。
可以在监听方法中写自己想要的功能代码。。。并且在上传中还包含了进度条信息可以看进度。。
<template>
<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>
</template>
import apis from "@apis";
import $ from "jquery";
import WebUploader from "webuploader";
export default {
data() {
return {
projectContract: { },
};
},
mounted() {
$("#btnSync").hide();
//该map,用于给uploader.options.formData表达赋一个动态的键值对.
var map = { };
// var testMd5;
//定义文件的分片大小
var chunkSize = 0.9 * 1024 * 1024;
var _self = this;
//监听分块上存过程中的三个时间点
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();
// setTimeout(() => {
// if (!map[file.id]) {
// // deferred.reject();
// alert("文件解析出错,请刷新后重新上传.");
// return deferred.promise();
// }
// }, 1000);
//显示暂停按键并且隐藏删除按键
switchButton(file.id);
//先查看服务器中是否已经有该文件
$.ajax({
type: "POST",
dataType: "json",
url: url_second_pass,//这是检查是否传过的请求路径
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("span.state")
.text("已经上传");
// deferred.resolve();
} else {
$("#" + file.id)
.find("span.state")
.text("正在上传...");
deferred.resolve();
}
},
error: function (data) {
console.log("秒传错误");
deferred.reject();
$("#" + file.id)
.find("span.state")
.text("秒传出错...");
alert("网络错误,请刷新后再上传");
},
});
return deferred.promise();
},
//时间点2:如果有分块上传,则每个分块上传之前调用此函数
//用于文件的续传
beforeSend: function (block) {
console.log("执行时间点2的方法。。。");
var deferred = WebUploader.Deferred();
$.ajax({
type: "POST",
dataType: "json",
url: url_check_chunk,//这里是检查文件分块的请求路径
data: {
chunk: block.chunk,
//block.file.id,获取该分片对应的文件的id,从而获取该文件的md5值
md5Val: map[block.file.id],
},
success: function (data) {
console.log("成功切块", 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("span.state")
.text("后台正在合并文件...");
//上传成功后,异步请求后台的servlet,发送的数据有guid(该文件所有分片保存的目录),chunks(该文件一共分了多少片,注意要向上取整),filename(文件名)
$.ajax({
type: "POST",
dataType: "json",
url: apis.url_merge_chunk,//这里是合并文件的请求路径
data: {
guid: uploader.options.formData.guid,
fileSize: file.size,
chunks: Math.ceil(file.size / chunkSize),
fileName: file.name,
md5Val: map[file.id],
},
success: (data) => {
if (data.status === "success") {
$("#" + file.id)
.find("span.state")
.text("已经上传");
} else {
$("#" + file.id)
.find("span.state")
.text("上传失败");
}
},
error: function (data) {
console.log("合并错误");
$("#" + file.id)
.find("span.state")
.text("合并文件出错...");
},
});
console.log("执行时间点3的方法。。。");
},
}
);
var uploader = WebUploader.create({
// swf文件路径
swf: "webuploader/Uploader.swf",
// 文件接收服务端。
server: apis.url_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,
//禁用全局拖拽功能
disableGlobalDnd: true,
// 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
resize: false,
});
// 当有文件被添加进队列的时候触发
uploader.on("fileQueued", function (file) {
// file.statusText = "未上传"
console.log("添加", file);
$("#thelist").append(
'<div id="' +
file.id +
'" class="item">' +
'<span class="info">' +
file.name +
"</span>" +
'<span class="state">等待上传...</span>' +
'<button class="btn btn-info btn-stop" style="display:none;">' +
"暂停" +
"</button>" +
'<a id="del" 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%;" >' +
"</div>" +
"</div>" +
"</div>"
);
new WebUploader.Uploader()
.md5File(file, 0, 10 * 1024 * 1024)
.progress(function (percentage) {
$("#" + file.id)
.find("span.state")
.text("正在读取文件信息..." + parseInt(percentage * 100) + "%");
})
//当文件读取完后,就执行then方法
.then(function (md5Val) {
map[file.id] = md5Val;
$("#" + file.id)
.find("span.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");
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;
//渐变的效果的消失
$(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) + "%"
);
});
uploader.on("uploadSuccess", (file) => {
switchButton(file.id);
console.log("执行上传成功的方法。。");
$("#" + file.id)
.find("span.state")
.text("上传成功。。。");
});
uploader.on("uploadError", function (file) {
switchButton(file.id);
$("#" + file.id)
.find("span.state")
.text("uploadError上传出错...");
});
//不管所有分片发送成功或者失败都会执行该方法
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;
}
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");
}
}
},
methods: {
},
};
</script>
<style lang="scss" scope>
</style>