前言
因為項目需要上傳大文件,考慮使用分片上傳锋恬、斷點續(xù)傳這些功能故選用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中參數(shù)定義
options:{
target:"/file/fdfs/multipart-upload/chunkUpload",//即分片上傳的URL
chunkSize: 10 * 1024 * 1024,//分片大小
simultaneousUploads:3,//并發(fā)上傳數(shù)与学,默認(rèn) 3
testChunks: true,//是否開啟服務(wù)器分片校驗
checkChunkUploadedByResponse: function (chunk, res) {// 服務(wù)器分片校驗函數(shù)彤悔,秒傳及斷點續(xù)傳基礎(chǔ)
//需后臺給出對應(yīng)的查詢分片的接口進行分片文件驗證
let objMessage = JSON.parse(res);//skipUpload、uploaded 需自己跟后臺商量好參數(shù)名稱
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
maxChunkRetries: 2, //最大自動失敗重試上傳次數(shù)
headers: {//在header中添加token驗證索守,Authorization請根據(jù)實際業(yè)務(wù)來
getUploadHeaders()
},
processParams(params) {//自定義每一次分片傳給后臺的參數(shù)晕窑,params是該方法返回的形參,包含分片信息,
//若不自定義則默認(rèn)會把文件所有參數(shù)傳給后臺卵佛,自己可以通過接口查看插件本身傳遞的參數(shù)
return {//返回一個對象杨赤,會添加到每一個分片的請求參數(shù)里面
totalChunks: params.totalChunks,
identifier:params.identifier,
chunkNumber: params.chunkNumber,
chunkSize: 10 * 1024 * 1024,
// 這是我跟后臺約定好的參數(shù)蓝丙,可根據(jù)自己項目實際情況改變
};
},
}
文件鉤子的使用
主要講一下幾個文件上傳鉤子和實例方法的使用,由于官方文檔寫的也不是很清楚望拖,踩了很多坑
代碼僅供參考
onFileAdded(file,fileList) {
file.pause()
if(file.getType()!='zip'){
EventBus.$emit('alert.show', {type: 'warning', msg: `只能上傳.zip格式的文件`});
file.ignored = true//文件校驗渺尘,不符規(guī)則的文件過濾掉
// 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{
// 新增文件的時候觸發(fā),計算MD5
this.myMD5(file);
}
},
onFileSuccess(rootFile, file, response, chunk){
//文件成功的時候觸發(fā)
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);//業(yè)務(wù)需求以文件名作為加密
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 //多余的參數(shù)可注釋
});
//繼續(xù)上傳文件
file.resume();
}else{
EventBus.$emit('alert.show', {type: 'warning', msg: `該文件已上傳至列表说敏,請勿重復(fù)上傳`});
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)相當(dāng)于before-upload鸥跟,即在上傳文件之前對文件進行的操作,一般用來做文件驗證盔沫。如果沒有配置autoStart=false點擊上傳文件按鈕之后文件會自動上傳
autoStart {Boolean}
默認(rèn) true, 是否選擇文件后自動開始上傳医咨。
我們要在onFileAdded()中通過pause()方法暫停文件上傳
onFileAdded(file,fileList) {
file.pause()//用來暫停文件上傳
if(file.getType()!='zip'){//根據(jù)自己項目要求進行文件驗證
EventBus.$emit('alert.show', {type: 'warning', msg: `只能上傳.zip格式的文件`});
file.ignored = true//文件校驗,不符規(guī)則的文件過濾掉
// file.cancel()//清除文件
// return false;
}
}
這里要講的是file.cancel()和file.ignored = true拟淮。
先講一下這個插件獲取uploader實例的方法
const uploaderInstance = this.$refs.uploader.uploader
uploaderInstance 就是uploader實例
在項目中點擊上傳文件按鈕(第一次上傳)谴忧,文件成功加入fileList之后,再次上傳此文件(第二次)沾谓,會進入onFileAdded()進行文件驗證委造,因為我進行文件查重,重復(fù)的就不在上傳了昏兆,所以報錯之后使用uploader實例提供的file.cancel()方法清除文件妇穴,此后再有點擊文件上傳此文件都不會跳入onFileAdded()方法爬虱,更不會進行驗證和報錯,這對于用戶來說無疑是一個巨大的BUG腾它。
通過檢查log發(fā)現(xiàn),每次點擊上傳文件之后插件會在uploader實例中的files數(shù)組中加入當(dāng)前文件的信息携狭,用插件實例方法cancel()是清除不掉當(dāng)前文件的逛腿。如果使用uploaderInstance.cancel()則會清除包括正在上傳,暫停上傳单默,已上傳成功的所有的文件信息。使用file.ignored = true使驗證失敗的文件不加入uploader實例中的files數(shù)組引颈,則下次點擊上傳文件按鈕可再次調(diào)起onFileAdded()進行驗證
uploader實例中的fileList數(shù)組中的文件才是實際正在上傳文件列表蝙场,對于uploader實例中files,file,fileList這幾個數(shù)組的具體作用有待研究
2.@file-success="onFileSuccess" 文件上傳成功的回調(diào)
onFileSuccess(rootFile, file, response, chunk){
//文件成功的時候觸發(fā)
},
3.@file-progress="onFileProgress" 用來獲取文件的實時上傳進度
onFileProgress(rootFile, file, chunk){
let index = this.findFileById(file.uniqueIdentifier),//通過index來獲取對應(yīng)的文件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" 文件上傳失敗的回調(diào)
onFileError(rootFile, file, response, chunk){
//文件上傳失敗的回調(diào)
},
5.@file-removed="fileRemoved" 刪除文件的回調(diào)
也可自定義刪除的按鈕不使用此鉤子
delFile(e, f){
e.stopPropagation();
let txt = '是否刪除選中數(shù)據(jù)?';
if(f.id){
txt = '該資源還未提交信息售滤,確認(rèn)刪除選中數(shù)據(jù)?';
}
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(); //這句代碼是刪除所選上傳文件的關(guān)鍵
}
this.delFileByFileuid(f.uid);
}).catch(console.error);
},
6.文件的暫停上傳和繼續(xù)上傳
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點中對刪除文件,暫停赐俗,繼續(xù)上傳都需要通過獲取uploader實例中fileList對應(yīng)的Index來進行操作
即:
const uploaderInstance = this.$refs.uploader.uploader
let index = uploaderInstance.fileList.findIndex(e => e.uniqueIdentifier === f.uid)
uploaderInstance.fileList[index].pause();// resume();cancel();
7.關(guān)于斷點續(xù)傳
官方文檔是這樣介紹的
checkChunkUploadedByResponse 可選的函數(shù)用于根據(jù) XHR 響應(yīng)內(nèi)容檢測每個塊是否上傳成功了阻逮,傳入的參數(shù)是:Uploader.Chunk 實例以及請求響應(yīng)信息秩彤。這樣就沒必要上傳(測試)所有的塊了
checkChunkUploadedByResponse: function (chunk, res) {// 服務(wù)器分片校驗函數(shù),秒傳及斷點續(xù)傳基礎(chǔ)
//需后臺給出對應(yīng)的查詢分片的接口進行分片文件驗證
let objMessage = JSON.parse(res);//skipUpload币励、uploaded 需自己跟后臺商量好參數(shù)名稱
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
checkChunkUploadedByResponse()是options中的一個配置項珊拼,配置之后上傳文件之前會自動調(diào)起一個get請求獲取已上傳的分片信息澎现。返回的內(nèi)容自己跟后臺協(xié)商每辟,一般是返回已上傳的分片的index數(shù)組,然后前端通過對返回的分片index進行篩選跳過已上傳的分片妹蔽,只上傳未上傳的分片挠将。
-
MD5文件驗證
斷點續(xù)傳及秒傳的基礎(chǔ)是要計算文件的MD5,這是文件的唯一標(biāo)識乳丰,然后服務(wù)器根據(jù)MD5進行判斷,我這里主要是用來做文件查重判斷
我這里使用的加密工具是spark-md5内贮,可以通過npm來安裝
npm install spark-md5 --save
在當(dāng)前vue文件引用即可
import SparkMD5 from 'spark-md5';
file有個屬性是uniqueIdentifier,代表文件唯一標(biāo)示粘勒,我們把計算出來的MD5賦值給這個屬性 file.uniqueIdentifier = md5
9.關(guān)于file.getType()的坑
getType()方法用于獲取文件類型屎即。
file.getType() ===>'zip' 等文件類型
如我在onFileAdd方法中做的文件格式驗證
if(file.getType()!='zip'){
EventBus.$emit('alert.show', {type: 'warning', msg: `只能上傳.zip格式的文件`});
file.ignored = true
}
但是項目提測之后發(fā)現(xiàn)驗證文件格式老是出問題,檢查log發(fā)現(xiàn)在我的電腦上file.getType()返回的是zip,在測試的電腦上返回的是x-zip-compressed埃撵。另外又試了其他格式的文件輸出其文件格式如下圖虽另,發(fā)現(xiàn)兩臺電腦的zip格式文件返回的不一樣捂刺。關(guān)鍵的問題就在于不同電腦上返回的文件格式不一樣(有待研究該api),所以對于getType()的使用得根據(jù)自己項目來考慮自己獲取還是使用該api
提供一個解決方案森缠,截取file.name的值獲取后綴
file.name.substring(file.name.lastIndexOf(".")+1)
另外關(guān)于文件上傳的格式application/x-zip-compressed仪缸,application/zip,application/vnd.ms-excel……有興趣的小伙伴可以自行研究一下插件的底層代碼
功能完成
說明:兼容了el-upload的上傳按鈕,第4個文件是用el-upload按鈕進行上傳的