在React中使用WebUploader實現大文件分片上傳的踩坑日記!


前段時間公司項目有個大文件分片上傳的需求,項目是用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呢?於是便各種嘗試,最終衍生出了上述代碼,解決了這個進度條不渲染的,需求到此也是都實現了。。。
      })
    }
  }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM