前言
教育项目视频上传、大文件频频上传失败、进度到100%后,服务端响应时间过长,影响观感。
继而采取分片上传方式,*有:
Web Uploader:http://fex.baidu.com/webuploader/
vue-simple-uploader:https://github.com/simple-uploader/vue-uploader
我们先看下可以直接npm导入的 vue-simple-uploader
简介
vue-simple-uploader就是一个基于 simple-uploader.js 和Vue结合做的一个上传组件,
自带 UI,可覆盖、自定义。它支持文件、多文件、文件夹上传;支持拖拽文件、
文件夹上传;可暂停、继续上传;支持秒传;上传队列管理,支持最大并发上传;
分片上传;支持进度、预估剩余时间、出错自动重试、重传等操作。
-
安装
npm install vue-simple-uploader --save
-
main全局导入
import uploader from 'vue-simple-uploader'
import App from './App.vue'
Vue.use(uploader)
需求和使用
- 项目需求
1、视频文件
2、一次只能上传一个,再次上传的视频文件会替换第一次上传
3、进度控制到99%
4、其余自定义
2.template
<uploader
ref="uploader1"
:options="options"
:file-status-text="fileStatusText"
@upload-start="onUploadStart"
@file-added="onFileAdded"
@file-progress="onFileProgress"
@file-success="onFileSuccess"
@file-error="onFileError"
class="uploader-example">
<uploader-unsupport></uploader-unsupport>
<!-- <p>Drop files here to upload or</p> -->
<el-button size="small" @click="beforeUploade">点击上传</el-button>
<div>只能上传mp4,mov,avi格式</div>
<uploader-btn :attrs="attrs" v-show="false" ref="upload">选择视频文件</uploader-btn>
<!-- <uploader-btn :directory="true">select folder</uploader-btn> -->
<uploader-list v-show="!form.videoUrl"></uploader-list>
<div v-show="form.videoUrl">{{form.videoUrl}}</div>
</uploader>
分析解释:
1、 options:uploader属性配置
fileStatusText:上传结果信息提示
@upload-start="onUploadStart" // 上传开始监听
@file-added="onFileAdded" // 文件上传到服务器前操作,可用校验等
@file-progress="onFileProgress" // onFileProgress上传进度回调
@file-success="onFileSuccess" // 所有分片上传完毕执行
@file-error="onFileError" // 上传错误监听
2、<uploader-btn :attrs="attrs" v-show="false" ref="upload">选择视频文件</uploader-btn>
attrs:{}默认添加属性到input
3、思路分析
采用el-button 按钮的beforeUploade调取页面隐藏uploader-btn标签上传,
是为了点击上传按钮前多一步操作,用于重置已经存在的文件,保证始终只有一个文件
- data中的参数
// 文件上传参数
uploaderInstance:undefined, // 用于保存ref即uploader组件
options: {
headers: { Authorization: "Bearer " + getToken() }, // 接口必须
target: process.env.VUE_APP_BASE_API + "/file?type=1", // 接口url地址
testChunks: false,
chunkSize: 1024*1024*5, // 5MB 每片大小
simultaneousUploads: 3, //并发上传数
maxChunkRetries: 2, //最大自动失败重试上传次数
parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) { //格式化时间
return parsedTimeRemaining
.replace(/\syears?/, '年')
.replace(/\days?/, '天')
.replace(/\shours?/, '小时')
.replace(/\sminutes?/, '分钟')
.replace(/\sseconds?/, '秒')
},
testChunks: true, //开启服务端分片校验
// 服务器分片校验函数
checkChunkUploadedByResponse: (chunk, message) => {
let obj = JSON.parse(message);
if (obj.isExist) {
this.statusTextMap.success = '秒传文件';
return true;
}
return (obj.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
},
statusTextMap: {
success: '上传成功',
error: '上传出错了',
uploading: '上传中...',
paused: '暂停',
waiting: '等待中...',
cmd5: '计算md5...'
},
fileStatusText: (status, response) => {
return this.statusTextMap[status];
},
attrs: {
accept: 'video/*',
// multiple: false,
},
注意事项:
attrs: {
accept: 'video/*',
// multiple: false,
},
在这里设置禁止input多选无效,我们后面优化会修改源码进行控制以及进度条只让它显示99%
- methods
onUploadStart(file){
// 上传开始执行
},
beforeUploade(){
// .cancel() 取消上传且从文件列表中移除
this.$refs.uploader1.uploader.cancel()
this.form.videoUrl='' //保存最终上传成功的url地址
document.querySelector('.uploader-btn>input').click()
},
onFileAdded(file) { // 文件上传前的校验
// 计算MD5 下一章我会讲到
// this.computeMD5(file);
},
//计算MD5
computeMD5(file) {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
chunkSize = 2097152,
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0,
spark = new SparkMD5.ArrayBuffer(),
fileReader = new FileReader();
let time = new Date().getTime();
file.cmd5 = true;
fileReader.onload = (e) => {
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
//console.log(`第${currentChunk}分片解析完成, 开始第${currentChunk +1} / ${chunks}分片解析`);
let percent = Math.floor(currentChunk / chunks * 100);
file.cmd5progress = percent;
loadNext();
} else {
console.log('finished loading');
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 = () => {
console.warn('oops, something went wrong.');
file.cancel();
};
let loadNext = () => {
let start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
};
loadNext();
},
// 文件进度的回调
onFileProgress(rootFile, file, chunk) {
console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},
onFileSuccess(rootFile, file, response, chunk) {
let resp = JSON.parse(response);
console.log(resp)
this.form.videoUrl = resp.data.url
//合并分片 所有上传结束通知后端进行合并分片
// if (resp.code === 0 && resp.merge === true) {
// axios.post('http://localhost:9999/up.php?action=merge', {
// filename: file.name,
// identifier: file.uniqueIdentifier,
// totalSize: file.size,
// totalChunks: chunk.offset + 1
// }).then(function(res){
// if (res.code === 0) {
// console.log('上传成功')
// } else {
// console.log(res.message);
// }
// })
// .catch(function(error){
// console.log(error);
// });
// }
},
onFileError(rootFile, file, response, chunk) {
console.log('Error:', response)
},