在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> </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; //保存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; <span style="color: rgba(255, 0, 0, 1)">//更新文件状态,所有选择的文件保存在fileQueuedList中</span></div>
<div> let res = updateFileList.map(item=>{</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 && missChunks !== undefined && missChunks !== '') {</div>
<div> var flag = true;</div>
<div> for (var i = 0; i < 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> // 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({ <span style="color: rgba(255, 0, 0, 1)">//把实例保存到state中</span></div>
<div> uploader:uploader </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=>{</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, <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=>{ <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=>{ <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=>{ </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&&that.props.onClose() //关闭弹窗</div>
<div> that.props.getFileList&&that.props.getFileList() //刷新文件table</div>
<div> }</div>
<div> </div>
<div> });</div>
<div> </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> </div>
<div> }</div>
<div> </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)"> </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) //继续上传可以采用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}) //更新文件状态</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=>{</div>
<div> return item.status==="PAUSE"</div>
<div> })</div>
<div> // console.log(PausedFile)</div>
<div> if(PausedFile&&PausedFile.length>0){ //如果有已暂停的文件则从已暂停的文件中第一个开始上传</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&&fileQueuedList.reduce((data,current)=>{ <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&&this.props.saveFileStatus(res) <span style="color: rgba(255, 0, 0, 1)">//把所有添加的文件状态保存下来传给父组件。再有父组件通过props传给子组件</span></div>
<div> }</div>
<div> this.props.onClose&&this.props.onClose()</div>
<div> this.props.getFileList()</div>
<div>}</div>
<div> </div>
<div>componentDidMount(){</div>
<div>//挂载完成后获取父组件的props保存的文件状态</div>
<div>
<div> const {savedFileList} = that.props; <span style="color: rgba(255, 0, 0, 1)">//savedFileList保存了关闭弹窗后未上传完的任务列表</span></div>
<div> // console.log(savedFileList,'saved')</div>
<div> this.uploadOperate() <span style="color: rgba(255, 0, 0, 1)">//把WebUploader相关的代码统一写在了此函数中,挂载时调用,注册hook并生成WebUploader实例</span></div>
<div> if(savedFileList&&savedFileList.length>0){</div>
<div> this.setState({</div>
<div> fileQueuedList:savedFileList, <span style="color: rgba(255, 0, 0, 1)">//赋值,显示未完成的文件列表</span></div>
<div> },()=>{</div>
<div> const {uploader,fileQueuedList} = that.state;</div>
<div> let files = fileQueuedList.map(item=>{</div>
<div> return item.file</div>
<div> })</div>
<div> for(let i = 0; i < files.length;i++){ </div>
<div> <span style="background-color: rgba(255, 255, 0, 1)">uploader.removeFile(files,true)</span> </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> </p><br><br>
来源:https://www.cnblogs.com/AIonTheRoad/p/11252253.html
頁:
[1]