在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)
//遍历所有的未完成任务,移除任务后再重新添加,目的是这样会触发fileQueue事件,否则进来点继续上传只会触发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呢?于是便各种尝试,最终衍生出了上述代码,解决了这个进度条不渲染的,需求到此也是都实现了。。。
      })
    }
  }
}

 

posted @ 2019-07-26 19:04  杰哥斯坦森  阅读(4996)  评论(3编辑  收藏  举报