前言
因为项目需要上传大文件,考虑使用分片上传、断点续传这些功能故选用vue-simple-uploader,期间踩的一些坑及解决方法记录一下。
git文档链接
1.https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md
2.https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md
使用vue-simple-uploader
1.安装插件:
npm install vue-simple-uploader --save
2.main.js中初始化
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
3.在*.vue文件中使用
代码仅供参考
<uploader
ref="uploader"
class="avatar-uploader"
:options="options"
:file-status-text="statusText"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-progress="onFileProgress"
@file-error="onFileError"
>
<!-- :autoStart=true -->
<!-- @file-removed="fileRemoved" -->
<uploader-unsupport></uploader-unsupport>
<uploader-btn
:single=true
title="(800M以上)"
>大文件上传</uploader-btn>
</uploader>
<el-collapse-item v-for="f in fileList" :key="f.uid" :name="f.uid">
<template slot="title">
<el-col :span="6">
<span class="pull-left name" :title="f.name">{{f.name}}</span>
<span class="pull-left status" v-bind:class="{error: f.state === 2}" v-text="statusStr(f.state)"></span>
</el-col>
<el-col :span="6" v-if="f.progress < 100 && f.state !== 2">
<el-progress class="progress" :text-inside="true" :stroke-width="15" :percentage="f.progress"></el-progress>
</el-col>
<i class="el-icon-my-close close-btn" title="删除" @click="delFile($event, f)"></i>
<span v-show="f.showPP">
<i class="el-icon-my-play play-btn" v-show='!f.isPlayOrPause' title="开始" @click="playFile($event, f)"></i>
<i class="el-icon-my-pause pause-btn" v-show='f.isPlayOrPause' title="暂停" @click="pauseFile($event, f)"></i>
</span>
</template>
<el-row ref="formWrapper">
<form-upload :info.sync="formObj" :file.sync="f" @deleteFile="delFileByFileuid" @expand="expandCollapse"></form-upload>
</el-row>
</el-collapse-item>
4.data中参数定义
options:{
target:"/file/fdfs/multipart-upload/chunkUpload",//即分片上传的URL
chunkSize: 10 * 1024 * 1024,//分片大小
simultaneousUploads:3,//并发上传数,默认 3
testChunks: true,//是否开启服务器分片校验
checkChunkUploadedByResponse: function (chunk, res) {// 服务器分片校验函数,秒传及断点续传基础
//需后台给出对应的查询分片的接口进行分片文件验证
let objMessage = JSON.parse(res);//skipUpload、uploaded 需自己跟后台商量好参数名称
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
maxChunkRetries: 2, //最大自动失败重试上传次数
headers: {//在header中添加token验证,Authorization请根据实际业务来
getUploadHeaders()
},
processParams(params) {//自定义每一次分片传给后台的参数,params是该方法返回的形参,包含分片信息,
//若不自定义则默认会把文件所有参数传给后台,自己可以通过接口查看插件本身传递的参数
return {//返回一个对象,会添加到每一个分片的请求参数里面
totalChunks: params.totalChunks,
identifier:params.identifier,
chunkNumber: params.chunkNumber,
chunkSize: 10 * 1024 * 1024,
// 这是我跟后台约定好的参数,可根据自己项目实际情况改变
};
},
}
文件钩子的使用
主要讲一下几个文件上传钩子和实例方法的使用,由于官方文档写的也不是很清楚,踩了很多坑
代码仅供参考
onFileAdded(file,fileList) {
file.pause()
if(file.getType()!='zip'){
EventBus.$emit('alert.show', {type: 'warning', msg: `只能上传.zip格式的文件`});
file.ignored = true//文件校验,不符规则的文件过滤掉
// file.cancel()
// return false;
}
else if(file.size<=800*1024*1024){
EventBus.$emit('alert.show', {type: 'warning', msg: `请上传800M以上的文件`});
file.ignored = true//文件过滤
}
else if(this.fileList.length >= 10){
EventBus.$emit('alert.show', {type: 'warning', msg: `最多上传10份文件`});
file.ignored = true//文件过滤
}else{
// 新增文件的时候触发,计算MD5
this.myMD5(file);
}
},
onFileSuccess(rootFile, file, response, chunk){
//文件成功的时候触发
let index = this.findFileById(file.uniqueIdentifier);
let res = JSON.parse(response)
if(res.result==="上传成功"){
this.uploadingFileStr(res.path,getUploadHeaders()).then(ress=>{
if(ress.data.msgCode===1){
if(index > -1){
this.fileList[index].id = ress.data.data.id;
this.fileList[index].resName = file.name.replace(/\.\w+$/g, '');
this.fileList[index].name = file.name;
this.fileList[index].filePath = null;
this.fileList[index].coverPath = null;
this.fileList[index].progress = 100;
this.fileList[index].status = "success";
this.fileList[index].state = 1;
this.fileList[index].isPlayOrPause=false;
this.fileList[index].showPP = false;
this.expandCollapse(file.uniqueIdentifier);
}
}else{
EventBus.$emit('alert.show', {type: 'error', msg: res.result});
if(index > -1){
this.fileList[index].status = 'fail';
this.fileList[index].state = 2;
this.fileList[index].isPlayOrPause=false;
this.fileList[index].showPP = false;
}
}
})
}
},
onFileError(rootFile, file, response, chunk){
let res = JSON.parse(response)
EventBus.$emit('alert.show', {type: 'error', msg: res.result});
let index = this.findFileById(file.uniqueIdentifier);
if(index > -1){
this.fileList[index].status = 'fail';
this.fileList[index].state = 2;
this.fileList[index].isPlayOrPause=false;
this.fileList[index].showPP = false;
}
},
onFileProgress(rootFile, file, chunk){
let index = this.findFileById(file.uniqueIdentifier),
p = Math.round(file.progress()*100);
if(index > -1){
if(p < 100){
this.fileList[index].progress = p;
}
this.fileList[index].status = file.status;
}
},
myMD5(file) {//这里主要是使用MD5对文件做一个上传的查重
let md5 = "";
md5 = SparkMD5.hash(file.name);//业务需求以文件名作为加密
let index = this.findFileById(md5);
if(index==-1){
file.uniqueIdentifier = md5;
this.fileList.push({
id: null,
uid: file.uniqueIdentifier,
filePath: '',
coverPath: '',
name: file.name,
resName: '',
progress: 0,
state: 0,
status: '',
isPlayOrPause:true,
showPP:true,
elMD5:md5 //多余的参数可注释
});
//继续上传文件
file.resume();
}else{
EventBus.$emit('alert.show', {type: 'warning', msg: `该文件已上传至列表,请勿重复上传`});
file.ignored = true//文件过滤
}
},
playFile(e,f){
e.stopPropagation();
f.isPlayOrPause=true
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)//兼容点击上传按钮
uploaderInstance.fileList[index].resume();
},
pauseFile(e,f){
e.stopPropagation();
f.isPlayOrPause=false
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
uploaderInstance.fileList[index].pause();
},
uoloader实例的一些方法和钩子
1.@file-added="onFileAdded" file-added(file,filelist)相当于before-upload,即在上传文件之前对文件进行的操作,一般用来做文件验证。如果没有配置autoStart=false点击上传文件按钮之后文件会自动上传
autoStart {Boolean}
默认 true, 是否选择文件后自动开始上传。
我们要在onFileAdded()中通过pause()方法暂停文件上传
onFileAdded(file,fileList) {
file.pause()//用来暂停文件上传
if(file.getType()!='zip'){//根据自己项目要求进行文件验证
EventBus.$emit('alert.show', {type: 'warning', msg: `只能上传.zip格式的文件`});
file.ignored = true//文件校验,不符规则的文件过滤掉
// file.cancel()//清除文件
// return false;
}
}
这里要讲的是file.cancel()和file.ignored = true。
先讲一下这个插件获取uploader实例的方法
const uploaderInstance = this.$refs.uploader.uploader
uploaderInstance 就是uploader实例
在项目中点击上传文件按钮(第一次上传),文件成功加入fileList之后,再次上传此文件(第二次),会进入onFileAdded()进行文件验证,因为我进行文件查重,重复的就不在上传了,所以报错之后使用uploader实例提供的file.cancel()方法清除文件,此后再有点击文件上传此文件都不会跳入onFileAdded()方法,更不会进行验证和报错,这对于用户来说无疑是一个巨大的BUG。
通过检查log发现,每次点击上传文件之后插件会在uploader实例中的files数组中加入当前文件的信息,用插件实例方法cancel()是清除不掉当前文件的。如果使用uploaderInstance.cancel()则会清除包括正在上传,暂停上传,已上传成功的所有的文件信息。使用file.ignored = true使验证失败的文件不加入uploader实例中的files数组,则下次点击上传文件按钮可再次调起onFileAdded()进行验证
uploader实例中的fileList数组中的文件才是实际正在上传文件列表,对于uploader实例中files,file,fileList这几个数组的具体作用有待研究
2.@file-success="onFileSuccess" 文件上传成功的回调
onFileSuccess(rootFile, file, response, chunk){
//文件成功的时候触发
},
3.@file-progress="onFileProgress" 用来获取文件的实时上传进度
onFileProgress(rootFile, file, chunk){
let index = this.findFileById(file.uniqueIdentifier),//通过index来获取对应的文件progress
p = Math.round(file.progress()*100);
if(index > -1){
if(p < 100){
this.fileList[index].progress = p;
}
this.fileList[index].status = file.status;
}
},
4.@file-error="onFileError" 文件上传失败的回调
onFileError(rootFile, file, response, chunk){
//文件上传失败的回调
},
5.@file-removed="fileRemoved" 删除文件的回调
也可自定义删除的按钮不使用此钩子
delFile(e, f){
e.stopPropagation();
let txt = '是否删除选中数据?';
if(f.id){
txt = '该资源还未提交信息,确认删除选中数据?';
}
this.$confirm(txt, '删除', {
type: 'warning',
showCancelButton: false
}).then(() => {
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
if(index>-1){
uploaderInstance.fileList[index].cancel(); //这句代码是删除所选上传文件的关键
}
this.delFileByFileuid(f.uid);
}).catch(console.error);
},
6.文件的暂停上传和继续上传
playFile(e,f){
e.stopPropagation();
f.isPlayOrPause=true
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)//兼容点击上传按钮
uploaderInstance.fileList[index].resume(); //
},
pauseFile(e,f){
e.stopPropagation();
f.isPlayOrPause=false
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
uploaderInstance.fileList[index].pause(); //
},
在第5和第6点中对删除文件,暂停,继续上传都需要通过获取uploader实例中fileList对应的Index来进行操作
即:
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
uploaderInstance.fileList[index].pause();// resume();cancel();
7.关于断点续传
官方文档是这样介绍的
checkChunkUploadedByResponse 可选的函数用于根据 XHR 响应内容检测每个块是否上传成功了,传入的参数是:Uploader.Chunk 实例以及请求响应信息。这样就没必要上传(测试)所有的块了
checkChunkUploadedByResponse: function (chunk, res) {// 服务器分片校验函数,秒传及断点续传基础
//需后台给出对应的查询分片的接口进行分片文件验证
let objMessage = JSON.parse(res);//skipUpload、uploaded 需自己跟后台商量好参数名称
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
checkChunkUploadedByResponse()是options中的一个配置项,配置之后上传文件之前会自动调起一个get请求获取已上传的分片信息。返回的内容自己跟后台协商,一般是返回已上传的分片的index数组,然后前端通过对返回的分片index进行筛选跳过已上传的分片,只上传未上传的分片。
-
MD5文件验证
断点续传及秒传的基础是要计算文件的MD5,这是文件的唯一标识,然后服务器根据MD5进行判断,我这里主要是用来做文件查重判断
我这里使用的加密工具是spark-md5,可以通过npm来安装
npm install spark-md5 --save
在当前vue文件引用即可
import SparkMD5 from 'spark-md5';
file有个属性是uniqueIdentifier,代表文件唯一标示,我们把计算出来的MD5赋值给这个属性 file.uniqueIdentifier = md5
9.关于file.getType()的坑
getType()方法用于获取文件类型。
file.getType() ===>'zip' 等文件类型
如我在onFileAdd方法中做的文件格式验证
if(file.getType()!='zip'){
EventBus.$emit('alert.show', {type: 'warning', msg: `只能上传.zip格式的文件`});
file.ignored = true
}
但是项目提测之后发现验证文件格式老是出问题,检查log发现在我的电脑上file.getType()返回的是zip,在测试的电脑上返回的是x-zip-compressed。另外又试了其他格式的文件输出其文件格式如下图,发现两台电脑的zip格式文件返回的不一样。关键的问题就在于不同电脑上返回的文件格式不一样(有待研究该api),所以对于getType()的使用得根据自己项目来考虑自己获取还是使用该api
提供一个解决方案,截取file.name的值获取后缀
file.name.substring(file.name.lastIndexOf(".")+1)
另外关于文件上传的格式application/x-zip-compressed,application/zip,application/vnd.ms-excel……有兴趣的小伙伴可以自行研究一下插件的底层代码
功能完成
说明:兼容了el-upload的上传按钮,第4个文件是用el-upload按钮进行上传的