前段时间公司项目有个大文件分片上传的需求,项目是用React写的,大文件分片上传这个功能使用了WebUploader这个组件。 具体交互是: 1. 点击上传文件button后出现弹窗,弹窗内有选择文
前段时间公司项目有个大文件分片上传的需求,项目是用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呢?于是便各种尝试,最终衍生出了上述代码,解决了这个进度条不渲染的,需求到此也是都实现了。。。 }) } } }