半个和尚 發表於 2019-7-26 19:04:00

在React中使用WebUploader实现大文件分片上传的踩坑日记!

<p>前段时间公司项目有个大文件分片上传的需求,项目是用React写的,大文件分片上传这个功能使用了WebUploader这个组件。</p>
<p>具体交互是:</p>
<p>1. 点击上传文件button后出现弹窗,弹窗内有选择文件和开始上传button。</p>
<p>2. 每个文件显示序号、文件名、进度条、上传操作按钮(开始/暂停、删除)。</p>
<p>3. 选择好文件之后点击开始上传,文件按照顺序自动从第一个开始上传。</p>
<p>4. 期间如果用户点了弹窗“X”关闭,则暂停任务,弹窗关闭。</p>
<p>5. 弹窗关闭之后重新点击上传文件button后将用户上次选择的未完成的文件展示出来,并可以继续上传。</p>
<p>6. 全部上传完成之后自动关闭弹窗。</p>
<p><img src="https://img2018.cnblogs.com/blog/1718183/201907/1718183-20190726163954817-2145934140.png" alt="" width="296" height="144"></p>
<p>&nbsp;</p>
<p>开发过程中踩了不少坑,好在自己始终没有放弃,慢慢研究探索,终于是实现了需求,或许这就叫做匠人精神吧😂😂。。</p>
<p>下面来分享一下开发过程中遇到的坑(博主React菜鸟一枚,写的不好勿喷,望各路大神指点😌)</p>
<p>首先说一下实现以上交互需求的具体思路吧:</p>
<p>注册uploader,在uploader实例化之后,把uploader保存在state里,在上传过程中更新文件状态,当上传完成时再更新一下状态。</p>
<p>更新状态的目的是后面会根据这些文件的状态渲染按钮,“待开始”状态的渲染“开始”按钮,“上传中”状态的渲染“暂停”按钮,已完成渲染“成功”按钮,“异常”状态的渲染“错误”按钮。</p>
<p><img src="https://img2018.cnblogs.com/blog/1718183/201907/1718183-20190726172413019-1442137657.png" alt="" width="339" height="173"></p>
<p>部分代码如下:</p>
<div class="cnblogs_code">
<div>//WebUploader hook</div>
<div>
<div>
<div>var chunkSize = 10 * 1024 * 1024;//分片上传,每片5M,默认是5M</div>
<div>var that = this;&nbsp; &nbsp;//保存this指针</div>
</div>
</div>
<div>WebUploader.Uploader.register({</div>
<div>  name:'my-uploader',</div>
<div>  'before-send-file': 'beforeSendFile',</div>
<div>  'before-send': 'beforeSend'</div>
<div>  }, {</div>
<div>  beforeSendFile: function (file) {</div>
<div>    // console.log("beforeSendFile");</div>
<div>    // Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。</div>
<div>    var task = new $.Deferred();</div>
<div>    // 根据文件内容来查询MD5</div>
<div>    uploader.md5File(file,0,chunkSize).progress(function (percentage) {})</div>
<div>       .then(function (val) { // md5计算完成</div>
<div>          // console.log('md5 result:', val);</div>
<div>          file.md5 = val;</div>
<div>          file.uid = WebUploader.Base.guid();</div>
<div>          // 进行md5判断</div>
<div>          $.post("后端checkMd5的url", {uid: file.uid, md5: file.md5, fileName:file.name},</div>
<div>            function (data) {</div>
<div>            // console.log(data,'md5 res');</div>
<div>              if(data.code=='500'){</div>
<div>                message.error(data.msg)</div>
<div>                let updateFileList = that.state.fileQueuedList;&nbsp; &nbsp;<span style="color: rgba(255, 0, 0, 1)">//更新文件状态,所有选择的文件保存在fileQueuedList中</span></div>
<div>                let res = updateFileList.map(item=&gt;{</div>
<div>                  if(item.fileId === file.id){</div>
<div>                    item.status = "ERROR";</div>
<div>                    item.statusName = "错误";</div>
<div>                  }</div>
<div>                  return item</div>
<div>                })</div>
<div>                that.setState({</div>
<div>                  fileQueuedList:res,</div>
<div>                })</div>
<div>                <span style="background-color: rgba(255, 255, 0, 1)">task.reject(); //遇到不符合要求的文件调用reject方法,可以上传后面正常的文件</span></div>
<div>              }else{</div>
<div>                var status = data.status.value;</div>
<div>                task.resolve();</div>
<div>                if (status == 101) {</div>
<div>                  // 文件不存在,那就正常流程</div>
<div>                }else if (status == 100) {</div>
<div>                  // 文件存在 忽略上传过程,直接标识上传成功;</div>
<div>                  message.error(file.name+data.msg);</div>
<div>                  uploader.skipFile(file);</div>
<div>                  file.pass = true;</div>
<div>                }else if (status == 102) {</div>
<div>                  // 部分已经上传到服务器了,但是差几个模块。</div>
<div>                  file.missChunks = data.data;</div>
<div>                }</div>
<div>              }</div>
<div>           }</div>
<div>        );</div>
<div>     });</div>
<div>     return $.when(task);</div>
<div>  },</div>
<div>  beforeSend: function (block) {</div>
<div>    var task = new $.Deferred();</div>
<div>    var file = block.file;</div>
<div>    var missChunks = file.missChunks;</div>
<div>    var blockChunk = block.chunk;</div>
<div>    // console.log("当前分块:" + blockChunk);</div>
<div>    // console.log("missChunks:" + missChunks);</div>
<div>    if (missChunks !== null &amp;&amp; missChunks !== undefined &amp;&amp; missChunks !== '') {</div>
<div>      var flag = true;</div>
<div>      for (var i = 0; i &lt; missChunks.length; i++) {</div>
<div>        if (blockChunk == missChunks) {</div>
<div>          // console.log(file.name + ":" + blockChunk + ":还没上传,现在上传去吧。");</div>
<div>          flag = false;</div>
<div>          break;</div>
<div>        }</div>
<div>      }</div>
<div>      if (flag) {</div>
<div>        task.reject();</div>
<div>      } else {</div>
<div>        task.resolve();</div>
<div>      }</div>
<div>    } else {</div>
<div>      task.resolve();</div>
<div>    }</div>
<div>    return $.when(task);</div>
<div>    }</div>
<div>  });</div>
<div>  // 实例化</div>
<div>  var uploader = WebUploader.create({</div>
<div>    pick: {</div>
<div>      id:'#picker',</div>
<div>      multiple:true</div>
<div>    },</div>
<div>    formData: {</div>
<div>      uid: 0,</div>
<div>      md5: '',</div>
<div>      chunkSize: chunkSize,</div>
<div>    },</div>
<div>    swf: '../webUploader/Uploader.swf', // swf文件路径</div>
<div>    chunked: true, //是否要分片处理大文件上传</div>
<div>    chunkSize: chunkSize,</div>
<div>    threads: 3, //上传并发数。允许同时最大上传进程数。</div>
<div>    server: '/dynamic/video/fileUpload', // 文件接收服务端。</div>
<div>    auto: false,</div>
<div>    duplicate:false,</div>
<div>    withCredentials:true,</div>
<div>    // accept: {</div>
<div>&nbsp; &nbsp;    // &nbsp; extensions: 'avi,asf,avs,mpg,mov,mp4,m4a,3gp,ogg,flv,ps,ts,dav,rmvb,SV4,SV5,SSDV',</div>
<div>    // },</div>
<div>    // 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。</div>
<div>    disableGlobalDnd: true,</div>
<div>    // fileNumLimit: 1024, //验证文件总数量, 超出则不允许加入队列。</div>
<div>    // fileSizeLimit: 1024 * 1024 * 1024, // 1G 验证文件总大小是否超出限制, 超出则不允许加入队列。</div>
<div>    // fileSingleSizeLimit: 20*1024 * 1024 * 1024 // 20G 验证单个文件大小是否超出限制, 超出则不允许加入队列。</div>
<div>  });</div>
<div>  that.setState({&nbsp; &nbsp; &nbsp; <span style="color: rgba(255, 0, 0, 1)">//把实例保存到state中</span></div>
<div>    uploader:uploader&nbsp; &nbsp;&nbsp;</div>
<div>  })</div>
<div>  // 当有文件被添加进队列的时候</div>
<div>  uploader.on('fileQueued', function (file) {</div>
<div>    let appendFile = that.state.fileQueuedList;</div>
<div>    let res = appendFile.some(item=&gt;{</div>
<div>      return item.file.name==file.name</div>
<div>    })</div>
<div>    if(res){</div>
<div>      // message.error(file.name+'文件重复。')</div>
<div>      return</div>
<div>    }</div>
<div>    appendFile.push({</div>
<div>      file:file,&nbsp; &nbsp; <span style="color: rgba(255, 0, 0, 1)">//把file对象也保存下来</span></div>
<div>      fileId:file.id,</div>
<div>      progress:'0%',</div>
<div>      status:'START',</div>
<div>      statusName:'待开始',</div>
<div>    })</div>
<div>    that.setState({</div>
<div>      fileQueuedList:appendFile,</div>
<div>    })</div>
<div>  });</div>
<br>
<div>        //当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。</div>
<div>        uploader.onUploadBeforeSend = function (obj, data) {</div>
<div>                // console.log("onUploadBeforeSend");</div>
<div>                var file = obj.file;</div>
<div>                data.md5 = file.md5 || '';</div>
<div>                data.uid = file.uid;</div>
<div>        };</div>
<div>        // 上传中</div>
<div>        uploader.on('uploadProgress', function (file, percentage) {</div>
<div>                let updateFileList = that.state.fileQueuedList;</div>
<div>                let res = updateFileList.map(item=&gt;{&nbsp; &nbsp; &nbsp;<span style="color: rgba(255, 0, 0, 1)"> //文件上传中时更新文件状态和进度条</span></div>
<div>                      if(item.fileId === file.id){</div>
<div>                          item.progress=Math.floor(percentage * 100) + '%';</div>
<div>                          item.status = "UPLOADING";</div>
<div>                          item.statusName = "上传中";</div>
<div>                      }</div>
<div>                      return item</div>
<div>                })</div>
<div>                that.setState({</div>
<div>                      fileQueuedList:res,</div>
<div>                })</div>
<div>                // console.log(Math.floor(percentage * 100) + '%',file.name,'上传进度')</div>





<br>
<div>        });</div>
<div>        // 上传返回结果</div>
<div>        uploader.on('uploadSuccess', function (file) {</div>
<div>                // console.log('success')</div>
<div>                let updateFileList = that.state.fileQueuedList;</div>
<div>                let res = updateFileList.map(item=&gt;{&nbsp; &nbsp; <span style="color: rgba(255, 0, 0, 1)">//文件上传成功更新状态</span></div>
<div>                      if(item.fileId === file.id){</div>
<div>                          item.progress='100%';</div>
<div>                          item.status = "UPLOADED";</div>
<div>                          item.statusName = "已完成"</div>
<div>                      }</div>
<div>                      return item</div>
<div>                })</div>
<div>   <span style="color: rgba(255, 0, 0, 1)"> //判断是不是都上传完,可以将该判断放在uploadComplete函数中,uploadSuccess只监听的到已成功的文件,uploadComplete函数无论成功失败都可以监听到</span></div>
<div>                let isAllCompleted = updateFileList.every(item=&gt;{&nbsp; &nbsp;&nbsp;</div>
<div>                      return item.status==="UPLOADED"||item.status==="ERROR"</div>
<div>                })</div>
<div>                that.setState({</div>
<div>                      fileQueuedList:res,</div>
<div>                      isAllCompleted:isAllCompleted</div>
<div>                })</div>
<div>                if(isAllCompleted){//都上传成功之后</div>
<div>                      that.props.onClose&amp;&amp;that.props.onClose() //关闭弹窗</div>
<div>                      that.props.getFileList&amp;&amp;that.props.getFileList() //刷新文件table</div>
<div>                }</div>
<div>&nbsp;</div>
<div>        });</div>
<div>&nbsp;</div>
<div>        uploader.on('error', function (type,file) {</div>
<div>                // message.error("上传出错!请检查后重新上传!错误代码"+type);</div>
<div>                // if(type=='F_DUPLICATE'){</div>
<div>                   //   message.error(file.name+'文件重复')</div>
<div>                // }</div>
<div>              // if (type == "Q_TYPE_DENIED") {</div>
<div>                //   message.error("请上传视频格式文件");</div>
<div>              // }else {</div>
<div>                //   message.error("上传出错!请检查后重新上传!错误代码"+type);</div>
<div>              // }</div>
<div>        });</div>
<div>&nbsp;</div>
<div>    }</div>
<div>&nbsp;</div>
<div>//点击文件的"开始"Icon,obj为当前点击的文件对象,即currentItem in fileQueuedList</div>
<div>
<div>fileUpload(obj){</div>
<div>  const {uploader,fileQueuedList} = this.state;</div>
<div>  uploader.upload(obj.file)</div>
<div>  let updateObj = fileQueuedList;</div>
<div>  let idx = fileQueuedList.indexOf(obj);</div>
<div>  updateObj.status = "UPLOADING";</div>
<div>  updateObj.statusName = "上传中";</div>
<div>  this.setState({fileQueuedList:updateObj})</div>
<div>}</div>
<div>//点击暂停Icon</div>
<div>fileStop(obj){</div>
<div>  const {uploader,fileQueuedList} = this.state;</div>
<div>  <span style="background-color: rgba(255, 255, 0, 1)">uploader.cancelFile(obj.file) </span></div>
<div><span style="color: rgba(255, 0, 0, 1)">//此处为第一个坑,在API里暂停是调用stop方法,此处想要暂停指定文件,显然应该用stop(file)方法,</span></div>
<div><span style="color: rgba(255, 0, 0, 1)">然而实践之后发现调用stop(file)方法会报错 “Cannot read property 'file' of undefined”,</span></div>
<div><span style="color: rgba(255, 0, 0, 1)">之后再点击继续发现无法继续上传,没有发出请求。</span></div>
<div><span style="color: rgba(255, 0, 0, 1)">后来经过各种尝试后采用了cancelFile方法,可以暂停并继续,但此方法会标记文件为已取消状态,可以再次手动选择添加进队列,从而不触发文件重复的error监听。</span></div>
<div><span style="color: rgba(255, 0, 0, 1)">&nbsp;</span></div>
<div>  let idx = fileQueuedList.indexOf(obj);</div>
<div>  let updateObj = fileQueuedList;</div>
<div>  updateObj.status = "PAUSE";</div>
<div>  updateObj.statusName = "已暂停";</div>
<div>  this.setState({fileQueuedList:updateObj})</div>
<div>    }</div>
<div>//文件暂停时点击继续开始Icon</div>
<div>    fileContinue(obj){</div>
<div>        const {uploader,fileQueuedList} = this.state;</div>
<div>        <span style="background-color: rgba(255, 255, 0, 1)">uploader.retry(obj.file)&nbsp; //继续上传可以采用retry方法也可以使用upload方法</span></div>
<div>        let idx = fileQueuedList.indexOf(obj);</div>
<div>        let updateObj = fileQueuedList;</div>
<div>        updateObj.status = "UPLOADING";</div>
<div>        updateObj.statusName = "上传中";</div>
<div>        this.setState({fileQueuedList:updateObj})&nbsp; //更新文件状态</div>
<div>    }</div>
<div>//点击文件删除Icon</div>
<div>    clickDeleteIcon(obj){</div>
<div>        let that = this;</div>
<div>        const {uploader,fileQueuedList} = that.state;</div>
<div>        let updateObj = fileQueuedList;</div>
<div>        let idx = fileQueuedList.indexOf(obj);</div>
<div>        updateObj.splice(idx,1)</div>
<div>        uploader.cancelFile(obj.file);</div>
<div>        that.setState({fileQueuedList:updateObj})</div>
<div>    }</div>
<div>//点击开始上传按钮</div>
<div>    startUpload(){</div>
<div>        const{uploader,fileQueuedList} = this.state;</div>
<div>        let PausedFile = fileQueuedList.filter(item=&gt;{</div>
<div>                return item.status==="PAUSE"</div>
<div>        })</div>
<div>        // console.log(PausedFile)</div>
<div>        if(PausedFile&amp;&amp;PausedFile.length&gt;0){&nbsp; &nbsp; //如果有已暂停的文件则从已暂停的文件中第一个开始上传</div>
<div>                uploader.upload(PausedFile.file)</div>
<div>        }else{</div>
<div>                uploader.upload()</div>
<div>        }</div>
<div>    }</div>
<div>//弹窗关闭</div>
<div>
<div>onClose(){</div>
<div>  const {fileQueuedList,isAllCompleted,uploader} = this.state;</div>
<div>  if(!isAllCompleted){</div>
<div>  let res = fileQueuedList&amp;&amp;fileQueuedList.reduce((data,current)=&gt;{&nbsp; <span style="color: rgba(255, 0, 0, 1)">//把除了错误和上传完成的文件暂停</span></div>
<div>    if(current.status!=='UPLOADED'||current.status!=='ERROR'){</div>
<div>      current.status="PAUSE";  </div>
<div>      current.statusName="已暂停";</div>
<div>      uploader.stop(true);</div>
<div>      data.push(current)</div>
<div>    }</div>
<div>    return data</div>
<div>    },[])</div>
<div>    // console.log(res,'res')</div>
<div>    this.props.saveFileStatus&amp;&amp;this.props.saveFileStatus(res)&nbsp; <span style="color: rgba(255, 0, 0, 1)">//把所有添加的文件状态保存下来传给父组件。再有父组件通过props传给子组件</span></div>
<div>  }</div>
<div>    this.props.onClose&amp;&amp;this.props.onClose()</div>
<div>    this.props.getFileList()</div>
<div>}</div>
<div>&nbsp;</div>
<div>componentDidMount(){</div>
<div>//挂载完成后获取父组件的props保存的文件状态</div>
<div>
<div>  const {savedFileList} = that.props;&nbsp; <span style="color: rgba(255, 0, 0, 1)">//savedFileList保存了关闭弹窗后未上传完的任务列表</span></div>
<div>  // console.log(savedFileList,'saved')</div>
<div>    this.uploadOperate()&nbsp; <span style="color: rgba(255, 0, 0, 1)">//把WebUploader相关的代码统一写在了此函数中,挂载时调用,注册hook并生成WebUploader实例</span></div>
<div>    if(savedFileList&amp;&amp;savedFileList.length&gt;0){</div>
<div>      this.setState({</div>
<div>        fileQueuedList:savedFileList,&nbsp; &nbsp; <span style="color: rgba(255, 0, 0, 1)">//赋值,显示未完成的文件列表</span></div>
<div>      },()=&gt;{</div>
<div>        const {uploader,fileQueuedList} = that.state;</div>
<div>        let files = fileQueuedList.map(item=&gt;{</div>
<div>        return item.file</div>
<div>      })</div>
<div>      for(let i = 0; i &lt; files.length;i++){&nbsp; &nbsp;&nbsp;</div>
<div>        <span style="background-color: rgba(255, 255, 0, 1)">uploader.removeFile(files,true)</span>&nbsp; &nbsp;</div>
<div>      }</div>
<div>     <span style="background-color: rgba(255, 255, 0, 1)"> uploader.addFiles(files)</span></div>
<div><span style="color: rgba(255, 0, 0, 1)">//遍历所有的未完成任务,移除任务后再重新添加,目的是这</span>样会触发fileQ<span style="color: rgba(255, 0, 0, 1)">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呢?于是便各种尝试,最终衍生出了上述代码,解决了这个进度条不渲染的,需求到此也是都实现了。。。</span></div>
<div>      })</div>
<div>    }</div>



</div>
<div>  }</div>
<div>}</div>



</div>




</div>





</div>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/AIonTheRoad/p/11252253.html
頁: [1]
查看完整版本: 在React中使用WebUploader实现大文件分片上传的踩坑日记!