前段時間公司項目有個大文件分片上傳的需求,項目是用React寫的,大文件分片上傳這個功能使用了WebUploader這個組件。
具體交互是:
1. 點擊上傳文件button后出現彈窗,彈窗內有選擇文件和開始上傳button。
2. 每個文件顯示序號、文件名、進度條、上傳操作按鈕(開始/暫停、刪除)。
3. 選擇好文件之后點擊開始上傳,文件按照順序自動從第一個開始上傳。
4. 期間如果用戶點了彈窗“X”關閉,則暫停任務,彈窗關閉。
5. 彈窗關閉之后重新點擊上傳文件button后將用戶上次選擇的未完成的文件展示出來,並可以繼續上傳。
6. 全部上傳完成之后自動關閉彈窗。
開發過程中踩了不少坑,好在自己始終沒有放棄,慢慢研究探索,終於是實現了需求,或許這就叫做匠人精神吧😂😂。。
下面來分享一下開發過程中遇到的坑(博主React菜鳥一枚,寫的不好勿噴,望各路大神指點😌)
首先說一下實現以上交互需求的具體思路吧:
注冊uploader,在uploader實例化之后,把uploader保存在state里,在上傳過程中更新文件狀態,當上傳完成時再更新一下狀態。
更新狀態的目的是后面會根據這些文件的狀態渲染按鈕,“待開始”狀態的渲染“開始”按鈕,“上傳中”狀態的渲染“暫停”按鈕,已完成渲染“成功”按鈕,“異常”狀態的渲染“錯誤”按鈕。
部分代碼如下:
//WebUploader hook
var chunkSize = 10 * 1024 * 1024;//分片上傳,每片5M,默認是5M
var that = this; //保存this指針
WebUploader.Uploader.register({
name:'my-uploader',
'before-send-file': 'beforeSendFile',
'before-send': 'beforeSend'
}, {
beforeSendFile: function (file) {
// console.log("beforeSendFile");
// Deferred對象在鈎子回掉函數中經常要用到,用來處理需要等待的異步操作。
var task = new $.Deferred();
// 根據文件內容來查詢MD5
uploader.md5File(file,0,chunkSize).progress(function (percentage) {})
.then(function (val) { // md5計算完成
// console.log('md5 result:', val);
file.md5 = val;
file.uid = WebUploader.Base.guid();
// 進行md5判斷
$.post("后端checkMd5的url", {uid: file.uid, md5: file.md5, fileName:file.name},
function (data) {
// console.log(data,'md5 res');
if(data.code=='500'){
message.error(data.msg)
let updateFileList = that.state.fileQueuedList;
//更新文件狀態,所有選擇的文件保存在fileQueuedList中
let res = updateFileList.map(item=>{
if(item.fileId === file.id){
item.status = "ERROR";
item.statusName = "錯誤";
}
return item
})
that.setState({
fileQueuedList:res,
})
task.reject(); //遇到不符合要求的文件調用reject方法,可以上傳后面正常的文件
}else{
var status = data.status.value;
task.resolve();
if (status == 101) {
// 文件不存在,那就正常流程
}else if (status == 100) {
// 文件存在 忽略上傳過程,直接標識上傳成功;
message.error(file.name+data.msg);
uploader.skipFile(file);
file.pass = true;
}else if (status == 102) {
// 部分已經上傳到服務器了,但是差幾個模塊。
file.missChunks = data.data;
}
}
}
);
});
return $.when(task);
},
beforeSend: function (block) {
var task = new $.Deferred();
var file = block.file;
var missChunks = file.missChunks;
var blockChunk = block.chunk;
// console.log("當前分塊:" + blockChunk);
// console.log("missChunks:" + missChunks);
if (missChunks !== null && missChunks !== undefined && missChunks !== '') {
var flag = true;
for (var i = 0; i < missChunks.length; i++) {
if (blockChunk == missChunks[i]) {
// console.log(file.name + ":" + blockChunk + ":還沒上傳,現在上傳去吧。");
flag = false;
break;
}
}
if (flag) {
task.reject();
} else {
task.resolve();
}
} else {
task.resolve();
}
return $.when(task);
}
});
// 實例化
var uploader = WebUploader.create({
pick: {
id:'#picker',
multiple:true
},
formData: {
uid: 0,
md5: '',
chunkSize: chunkSize,
},
swf: '../webUploader/Uploader.swf', // swf文件路徑
chunked: true, //是否要分片處理大文件上傳
chunkSize: chunkSize,
threads: 3, //上傳並發數。允許同時最大上傳進程數。
server: '/dynamic/video/fileUpload', // 文件接收服務端。
auto: false,
duplicate:false,
withCredentials:true,
// accept: {
// extensions: 'avi,asf,avs,mpg,mov,mp4,m4a,3gp,ogg,flv,ps,ts,dav,rmvb,SV4,SV5,SSDV',
// },
// 禁掉全局的拖拽功能。這樣不會出現圖片拖進頁面的時候,把圖片打開。
disableGlobalDnd: true,
// fileNumLimit: 1024, //驗證文件總數量, 超出則不允許加入隊列。
// fileSizeLimit: 1024 * 1024 * 1024, // 1G 驗證文件總大小是否超出限制, 超出則不允許加入隊列。
// fileSingleSizeLimit: 20*1024 * 1024 * 1024 // 20G 驗證單個文件大小是否超出限制, 超出則不允許加入隊列。
});
that.setState({
//把實例保存到state中
uploader:uploader
})
// 當有文件被添加進隊列的時候
uploader.on('fileQueued', function (file) {
let appendFile = that.state.fileQueuedList;
let res = appendFile.some(item=>{
return item.file.name==file.name
})
if(res){
// message.error(file.name+'文件重復。')
return
}
appendFile.push({
file:file,
//把file對象也保存下來
fileId:file.id,
progress:'0%',
status:'START',
statusName:'待開始',
})
that.setState({
fileQueuedList:appendFile,
})
});
//當某個文件的分塊在發送前觸發,主要用來詢問是否要添加附帶參數,大文件在開起分片上傳的前提下此事件可能會觸發多次。
uploader.onUploadBeforeSend = function (obj, data) {
// console.log("onUploadBeforeSend");
var file = obj.file;
data.md5 = file.md5 || '';
data.uid = file.uid;
};
// 上傳中
uploader.on('uploadProgress', function (file, percentage) {
let updateFileList = that.state.fileQueuedList;
let res = updateFileList.map(item=>{
//文件上傳中時更新文件狀態和進度條
if(item.fileId === file.id){
item.progress=Math.floor(percentage * 100) + '%';
item.status = "UPLOADING";
item.statusName = "上傳中";
}
return item
})
that.setState({
fileQueuedList:res,
})
// console.log(Math.floor(percentage * 100) + '%',file.name,'上傳進度')
});
// 上傳返回結果
uploader.on('uploadSuccess', function (file) {
// console.log('success')
let updateFileList = that.state.fileQueuedList;
let res = updateFileList.map(item=>{
//文件上傳成功更新狀態
if(item.fileId === file.id){
item.progress='100%';
item.status = "UPLOADED";
item.statusName = "已完成"
}
return item
})
//判斷是不是都上傳完,可以將該判斷放在uploadComplete函數中,uploadSuccess只監聽的到已成功的文件,uploadComplete函數無論成功失敗都可以監聽到
let isAllCompleted = updateFileList.every(item=>{
return item.status==="UPLOADED"||item.status==="ERROR"
})
that.setState({
fileQueuedList:res,
isAllCompleted:isAllCompleted
})
if(isAllCompleted){ //都上傳成功之后
that.props.onClose&&that.props.onClose() //關閉彈窗
that.props.getFileList&&that.props.getFileList() //刷新文件table
}
});
uploader.on('error', function (type,file) {
// message.error("上傳出錯!請檢查后重新上傳!錯誤代碼"+type);
// if(type=='F_DUPLICATE'){
// message.error(file.name+'文件重復')
// }
// if (type == "Q_TYPE_DENIED") {
// message.error("請上傳視頻格式文件");
// }else {
// message.error("上傳出錯!請檢查后重新上傳!錯誤代碼"+type);
// }
});
}
//點擊文件的"開始"Icon,obj為當前點擊的文件對象,即currentItem in fileQueuedList
fileUpload(obj){
const {uploader,fileQueuedList} = this.state;
uploader.upload(obj.file)
let updateObj = fileQueuedList;
let idx = fileQueuedList.indexOf(obj);
updateObj[idx].status = "UPLOADING";
updateObj[idx].statusName = "上傳中";
this.setState({fileQueuedList:updateObj})
}
//點擊暫停Icon
fileStop(obj){
const {uploader,fileQueuedList} = this.state;
uploader.cancelFile(obj.file)
//此處為第一個坑,在API里暫停是調用stop方法,此處想要暫停指定文件,顯然應該用stop(file)方法,
然而實踐之后發現調用stop(file)方法會報錯 “Cannot read property 'file' of undefined”,
之后再點擊繼續發現無法繼續上傳,沒有發出請求。
后來經過各種嘗試后采用了cancelFile方法,可以暫停並繼續,但此方法會標記文件為已取消狀態,可以再次手動選擇添加進隊列,從而不觸發文件重復的error監聽。
let idx = fileQueuedList.indexOf(obj);
let updateObj = fileQueuedList;
updateObj[idx].status = "PAUSE";
updateObj[idx].statusName = "已暫停";
this.setState({fileQueuedList:updateObj})
}
//文件暫停時點擊繼續開始Icon
fileContinue(obj){
const {uploader,fileQueuedList} = this.state;
uploader.retry(obj.file) //繼續上傳可以采用retry方法也可以使用upload方法
let idx = fileQueuedList.indexOf(obj);
let updateObj = fileQueuedList;
updateObj[idx].status = "UPLOADING";
updateObj[idx].statusName = "上傳中";
this.setState({fileQueuedList:updateObj}) //更新文件狀態
}
//點擊文件刪除Icon
clickDeleteIcon(obj){
let that = this;
const {uploader,fileQueuedList} = that.state;
let updateObj = fileQueuedList;
let idx = fileQueuedList.indexOf(obj);
updateObj.splice(idx,1)
uploader.cancelFile(obj.file);
that.setState({fileQueuedList:updateObj})
}
//點擊開始上傳按鈕
startUpload(){
const{uploader,fileQueuedList} = this.state;
let PausedFile = fileQueuedList.filter(item=>{
return item.status==="PAUSE"
})
// console.log(PausedFile)
if(PausedFile&&PausedFile.length>0){ //如果有已暫停的文件則從已暫停的文件中第一個開始上傳
uploader.upload(PausedFile[0].file)
}else{
uploader.upload()
}
}
//彈窗關閉
onClose(){
const {fileQueuedList,isAllCompleted,uploader} = this.state;
if(!isAllCompleted){
let res = fileQueuedList&&fileQueuedList.reduce((data,current)=>{
//把除了錯誤和上傳完成的文件暫停
if(current.status!=='UPLOADED'||current.status!=='ERROR'){
current.status="PAUSE";
current.statusName="已暫停";
uploader.stop(true);
data.push(current)
}
return data
},[])
// console.log(res,'res')
this.props.saveFileStatus&&this.props.saveFileStatus(res)
//把所有添加的文件狀態保存下來傳給父組件。再有父組件通過props傳給子組件
}
this.props.onClose&&this.props.onClose()
this.props.getFileList()
}
componentDidMount(){
//掛載完成后獲取父組件的props保存的文件狀態
const {savedFileList} = that.props;
//savedFileList保存了關閉彈窗后未上傳完的任務列表
// console.log(savedFileList,'saved')
this.uploadOperate()
//把WebUploader相關的代碼統一寫在了此函數中,掛載時調用,注冊hook並生成WebUploader實例
if(savedFileList&&savedFileList.length>0){
this.setState({
fileQueuedList:savedFileList,
//賦值,顯示未完成的文件列表
},()=>{
const {uploader,fileQueuedList} = that.state;
let files = fileQueuedList.map(item=>{
return item.file
})
for(let i = 0; i < files.length;i++){
uploader.removeFile(files[i],true)
}
uploader.addFiles(files)
//遍歷所有的未完成任務,移除任務后再重新添加,目的是這樣會觸發fileQ
ueue事件,否則進來點繼續上傳只會觸發uploadProgress函數,在這個函數里有setState方法,但是會報錯“Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component.” 發現上傳請求是正常進行的,但是頁面進度條不渲染,這也是第二個坑點,博主當時也沒有找到原因,因為componentDidMount函數已經觸發了,uploader實例也生成了,為什么還是unmounted component呢?於是便各種嘗試,最終衍生出了上述代碼,解決了這個進度條不渲染的,需求到此也是都實現了。。。
})
}
}
}