springboot整合vue2实现大文件分片上传、秒传、断点续传
实现说明
本篇文章主要介绍vue端的整合和技术知识点的分析,springboot后台代码及整合请参考上一篇文章:springboot整合vue2-uploader文件分片上传、秒传、断点续传
项目开源地址:
https://gitee.com/msxy/qingfeng-vue
其他产品开源地址:https://gitee.com/msxy
安装uploader和spark-md5的依赖
npm install --save vue-simple-uploader
npm install --save spark-md5
mainjs导入uploader
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
创建uploader组件
将文件拖放到此处以上传
选择文件
选择图片
选择文件夹
全部开始
全部暂停
全部移除
Uploader组件使用
资源上传
核心方法分析
将文件拖放到此处以上传
选择文件
选择图片
选择文件夹
全部开始
全部暂停
全部移除
options参数分析
参考 simple-uploader.js 配置。此外,你可以有如下配置项可选:
parseTimeRemaining(timeRemaining, parsedTimeRemaining) {Function}用于格式化你想要剩余时间,一般可以用来做多语言。参数:
- timeRemaining{Number}, 剩余时间,秒为单位
- parsedTimeRemaining{String}, 默认展示的剩余时间内容,你也可以这样做替换使用:
parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
return parsedTimeRemaining
.replace(/\syears?/, '年')
.replace(/\days?/, '天')
.replace(/\shours?/, '小时')
.replace(/\sminutes?/, '分钟')
.replace(/\sseconds?/, '秒')
}
categoryMap {Object}
文件类型 map,默认:
{
image: ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp'],
video: ['mp4', 'm3u8', 'rmvb', 'avi', 'swf', '3gp', 'mkv', 'flv'],
audio: ['mp3', 'wav', 'wma', 'ogg', 'aac', 'flac'],
document: ['doc', 'txt', 'docx', 'pages', 'epub', 'pdf', 'numbers', 'csv', 'xls', 'xlsx', 'keynote', 'ppt', 'pptx']
}
autoStart {Boolean}默认 true, 是否选择文件后自动开始上传。
fileStatusText {Object}默认:
{
success: 'success',
error: 'error',
uploading: 'uploading',
paused: 'paused',
waiting: 'waiting'
}
用于转换文件上传状态文本映射对象。
0.6.0 版本之后,fileStatusText 可以设置为一个函数,参数为 (status, response = null), 第一个 status 为状态,第二个为响应内容,默认 null,示例:
fileStatusText(status, response) {
const statusTextMap = {
uploading: 'uploading',
paused: 'paused',
waiting: 'waiting'
}
if (status === 'success' || status === 'error') {
// 只有status为success或者error的时候可以使用 response
// eg:
// return response data ?
return response.data
} else {
return statusTextMap[status]
}
}
fileComplete方法
fileComplete(rootFile) {
// 一个根文件(文件夹)成功上传完成。
// console.log("fileComplete", rootFile);
// console.log("一个根文件(文件夹)成功上传完成。");
},
complete方法
complete() {
// 上传完毕。
// console.log("complete");
},
fileSuccess方法
文件上传成功,进行合并。
fileSuccess(rootFile, file, response, chunk) {
// console.log(rootFile);
// console.log(file);
// console.log(message);
// console.log(chunk);
const result = JSON.parse(response);
console.log(result.success, this.skip);
if (result.success && !this.skip) {
axios
.post(
"http://127.0.0.1:8888/upload/merge",
{
identifier: file.uniqueIdentifier,
filename: file.name,
totalChunks: chunk.offset,
},
{
headers: { "Access-Token": storage.get(ACCESS_TOKEN) }
}
)
.then((res) => {
if (res.data.success) {
console.log("上传成功");
} else {
console.log(res);
}
})
.catch(function (error) {
console.log(error);
});
} else {
console.log("上传成功,不需要合并");
}
if (this.skip) {
this.skip = false;
}
},
filesAdded方法
文件选择完成,进行文件分片处理。
filesAdded(file, fileList, event) {
// console.log(file);
file.forEach((e) => {
this.fileList.push(e);
this.computeMD5(e);
});
},
computeMD5(file) {
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 1024 * 1024;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
// 文件状态设为"计算MD5"
file.cmd5 = true; //文件状态为“计算md5...”
file.pause();
loadNext();
fileReader.onload = (e) => {
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
// 实时展示MD5的计算进度
console.log(
`第${currentChunk}分片解析完成, 开始第${
currentChunk + 1
} / ${chunks}分片解析`
);
} else {
let md5 = spark.end();
console.log(
`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
file.size
} 用时:${new Date().getTime() - time} ms`
);
spark.destroy(); //释放缓存
file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
file.cmd5 = false; //取消计算md5状态
file.resume(); //开始上传
}
};
fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`);
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
allStart全部开始
allStart() {
console.log(this.fileList);
this.fileList.map((e) => {
if (e.paused) {
e.resume();
}
});
},
allStop全部停止
allStop() {
console.log(this.fileList);
this.fileList.map((e) => {
if (!e.paused) {
e.pause();
}
});
},
allRemove全部移除
allRemove() {
this.fileList.map((e) => {
e.cancel();
});
this.fileList = [];
},
文件分片
vue-simple-uploader自动将文件进行分片,在options的chunkSize中可以设置每个分片的大小。
如图:对于大文件来说,会发送多个请求,在设置testChunks为true后(在插件中默认就是true),会发送与服务器进行分片校验的请求,下面的第一个get请求就是该请求;后面的每一个post请求都是上传分片的请求
看一下发送给服务端的参数,其中chunkNumber表示当前是第几个分片,totalChunks代表所有的分片数,这两个参数都是都是插件根据你设置的chunkSize来计算的。
需要注意的就是在最后文件上传成功的事件中,通过后台返回的字段,来判断是否要再给后台发送一个文件合并的请求。
MD5的计算过程
断点续传及秒传的基础是要计算文件的MD5,这是文件的唯一标识,然后服务器根据MD5进行判断,是进行秒传还是断点续传。
在file-added事件之后,就计算MD5,我们最终的目的是将计算出来的MD5加到参数里传给后台,然后继续文件上传的操作,详细的思路步骤是:
- 把uploader组件的autoStart设为false,即选择文件后不会自动开始上传
- 先通过 file.pause()暂停文件,然后通过H5的FileReader接口读取文件
- 将异步读取文件的结果进行MD5,这里我用的加密工具是spark-md5,你可以通过npm install spark-md5 --save来安装,也可以使用其他MD5加密工具。
- file有个属性是uniqueIdentifier,代表文件唯一标示,我们把计算出来的MD5赋值给这个属性 file.uniqueIdentifier = md5,这就实现了我们最终的目的。
- 通过file.resume()开始/继续文件上传。
computeMD5(file) {
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 1024 * 1024;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
// 文件状态设为"计算MD5"
file.cmd5 = true; //文件状态为“计算md5...”
file.pause();
loadNext();
fileReader.onload = (e) => {
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
// 实时展示MD5的计算进度
console.log(
`第${currentChunk}分片解析完成, 开始第${
currentChunk + 1
} / ${chunks}分片解析`
);
} else {
let md5 = spark.end();
console.log(
`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
file.size
} 用时:${new Date().getTime() - time} ms`
);
spark.destroy(); //释放缓存
file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
file.cmd5 = false; //取消计算md5状态
file.resume(); //开始上传
}
};
fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`);
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
给file的uniqueIdentifier 属性赋值后,请求中的identifier即是我们计算出来的MD5
秒传及断点续传
在计算完MD5后,我们就能谈断点续传及秒传的概念了。
服务器根据前端传过来的MD5去判断是否可以进行秒传或断点续传:
- a. 服务器发现文件已经完全上传成功,则直接返回秒传的标识。
- b. 服务器发现文件上传过分片信息,则返回这些分片信息,告诉前端继续上传,即断点续传。
在每次上传过程的最开始,vue-simple-uploader会发送一个get请求,来问服务器我哪些分片已经上传过了,这个请求返回的结果也有几种可能:
- a. 如果是秒传,在请求结果中会有相应的标识,比如我这里是skipUpload为true,且返回了url,代表服务器告诉我们这个文件已经有了,我直接把url给你,你不用再传了,这就是秒传。
- b. 如果后台返回了分片信息,这是断点续传。如图,返回的数据中有个uploaded的字段,代表这些分片是已经上传过的了,插件会自动跳过这些分片的上传。
图b1:断点续传情况下后台返回值
前端做分片检验:checkChunkUploadedByResponse
插件自己是不会判断哪个需要跳过的,在代码中由options中的
checkChunkUploadedByResponse控制,它会根据 XHR 响应内容检测每个块是否上传成功了,成功的分片直接跳过上传
你要在这个函数中进行处理,可以跳过的情况下返回true即可。
checkChunkUploadedByResponse: function (chunk, message) {
let objMessage = JSON.parse(message);
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
注:skipUpload 和 uploaded 是我和后台商议的字段,你要按照后台实际返回的字段名来。
优化MD5计算
原uploader中计算MD5的方式为对整个文件直接计算MD5,很吃内存,容易导致浏览器崩溃
我改成了通过分片读取文件的方式计算MD5,防止直接读取大文件时因内存占用过大导致的网页卡顿、崩溃。
自定义的状态
(之前我就封装了几种自定义状态,最近总有小伙伴问怎么没有“校验MD5”,“合并中”这些状态,我就把我的方法写出来了,方式很笨,但是能实现效果)
插件原本只支持了success、error、uploading、paused、waiting这几种状态,
由于业务需求,我额外增加了“校验MD5”、“合并中”、“转码中”、“上传失败”这几种自定义的状态
由于前几种状态是插件已经封装好的,我不能改源码,只能用比较hack的方式:
当自定义状态开始时,要手动调一下statusSet方法,生成一个p标签盖在原本的状态上面;当自定义状态结束时,还要手动调用statusRemove移除该标签。
this.statusSet(file.id, 'merging');
this.statusRemove(file.id);