simple-uploader/Uploader

关于fileAdded事件,返回false时的处理,以及分片优化方案的建议

xyhxyw opened this issue · 15 comments

1 监听fileAdded事件,如果返回false,文件未加入上传队列(uploader.files),但却加入了uploader.fileList中并在页面显示了出来。
期望:返回了flase,应该表示该文件不是用户需要的文件,不应该加入uploader.fileList。
uploader.js 133行,可修改为:

     var _file = new File(this, file, this)
      _file.uniqueIdentifier = uniqueIdentifier
      var ignored = this._trigger('fileAdded', _file, evt);

      if (ignored) {
        _file.uploaed = 2;
        _files.push(_file)
      }else{
        this.removeFile(_file)
        //alert(_file.name+':格式不正确')
      }

2:进行分片上传时,有时候并不需要每个分片都做一次testChunks。这样对于较大文件,会浪费很多次请求。
建议,可增加一个参数enableOnceCheck。

enableOnceCheck为true时,在文件加入上传队列时,请求一次后台(GET),获取当前文件已经上传的分片位置,并保存到_file对象中。

如_file.uploadeChunk = 10,开始上传时(uploadNextChunk),直接从第11个分片开始POST(并且此时不需要再做分片检测的GET请求)。这种方案对于大文件,可以节省大量的GET检测请求。

enableOnceCheck为false时,默认状态,依旧对每个分片进行GET检测。

多谢反馈,关于 1 的 bug 已在 0.1.1 版本修复,关于 2,这种方式的确很赞,节省不少开销,作为下个版本功能增强 👍

已做了初步实现,,其中可改进的地方如下:

  • 发送get或post请求可以封装到utils中,作为通用的请求方法。同时,当需要同时请求多个文件的状态时,可考虑引入Promise。
  • chunk是否完成,都是通过xhr的state状态来判断的。因此,enableOnceCheck为true时,要主动触发chunk的success事件,还需要保证chunk的xhr状态为已完成(因此我在代码中挂载了一个已完成的xhr对象到chunk中)。

具体实现如下,enableOnceCheck默认为false(代码比较粗糙,有较多冗余的地方,望提出改正)。

在uploader.js145行,enableOnceCheck为false时,代码处理逻辑不变;为true时,则向后台发送get请求,返回文件对应的已上传最大分片编号(需要配合后台):

if(!enableOnceCheck){
        //代码不变
        utils.each(_files, function (file) {
          if (this.opts.singleFile && this.files.length > 0) {
            this.removeFile(this.files[0])
          }
          this.files.push(file);
        },this)
        this._trigger('filesSubmitted', _files, newFileList, evt)
}
else{
        //enableOnceCheck为true,返回每个文件已上传最大分片号
        var index = 0;
        utils.each(_files, function (file) {
          if (this.opts.singleFile && this.files.length > 0) {
            this.removeFile(this.files[0])
          }
          (function(f,_this,_index){
              var test = function(){
                //挂载xhr对象到file属性下
                f.xhr = new XMLHttpRequest();
                var testHandler = function(e){
                  var response = f.xhr.response;
                  //response格式为{maxChunkNumber:5},maxChunkNumber表示已上传的最大分片number
                  response = JSON.parse(response);
                  //当前已上传的分片位置
                  var uploaded = response.maxChunkNumber;
                  f.uploaded = uploaded;
                  _this.files.push(f);
                  //异步请求全部完成
                  if(_index==_files.length-1){
                    //触发filesSubmitted事件,并添加到上传队列
                    _this._trigger('filesSubmitted', _files, newFileList, evt)
                  }
                };
                f.xhr.addEventListener('load', testHandler, false);
                f.xhr.addEventListener('error', testHandler, false);
                f.xhr.addEventListener('timeout', testHandler, false);
                var params = _this.opts.target+"?resumableChunkNumber=1&resumableIdentifier="+encodeURIComponent(f.uniqueIdentifier)+"&resumableFilename="+encodeURIComponent(f.name);
                f.xhr.open(_this.opts.testMethod, params,true);
                f.xhr.timeout = _this.opts.xhrTimeout;
                f.xhr.send(null);
              };
              test();
            })(file,this,index);
          index++;
        },this)
}

在uploader.js文件,uploadNextChunk函数221-234行间:enableOnceCheck为false时,处理逻辑不变;为true时,则从uoloaded+1的位置开始,直接发送POST请求上传文件,并且不再做GET验证请求。

if(!enableOnceCheck){
     //代码不变
    utils.each(this.files, function (file) {
        if (!file.paused) {
          utils.each(file.chunks, function (chunk) {
            if (chunk.status() === pendingStatus) {
              chunk.send()
              found = true
              return false
            }
          })
        }
        if (found) {
          return false
        }
      })
}
else{
    utils.each(this.files, function(file){
        //已上传的最大分片编码
        var uploaded = file.uploaded;
        var xhr = file.xhr;
        if(!file.paused){
          utils.each(file.chunks, function(chunk){
            //表示该分片已上传完成
            if(chunk.offset<uploaded){
              //挂载xhr到chunk对象
              chunk.xhr =  xhr;
              //主动触发完成事件
              chunk._event('success');
            }else{
              if(chunk.status() === pendingStatus) {
                //强制设置tested为true,跳过test直接上传
                chunk.tested = true;
                chunk.send();
                found = true;
                return(false);
              }
            }
          });
        }
        if(found) return(false);
      });
}

非常感谢作者的开源,uploader上传插件很棒,正在您的基础上自己改进代码。

考虑到分块上传,上传成功的块不一定是线性的,有可能是成功的是 1 3 4 这样的,所以目前实现是支持 checkChunkUploadedByResponse(用于检查每个文件中每个块是否已经上传成功), 参见 6baf0be 这个提交;示例使用的话就是参照 ab9f6c6 这里的改动。

@xyhxyw 看是否能满足期望 😁 (目前还在 develop 分支)

  1. @dolymood ,好像并没有看到你是在何处得到已经上传的分片数组啊?您给的是一个思路是吧?
  2. 上传成功的块确实不一定是线性的,所以,我们后台的处理方式时,返回连续块中最小的那个。如果已上传1 3 4,这个时候返回的就是1。

@xyhxyw 不是思路,看这里:

checkChunkUploadedByResponse: function (chunk, message) {
var objMessage = {}
try {
objMessage = JSON.parse(message)
} catch (e) {}
// fake response
// objMessage.uploaded_chunks = [2, 3, 4, 5, 6, 8, 10, 11, 12, 13, 17, 20, 21]
// check the chunk is uploaded
return (objMessage.uploaded_chunks || []).indexOf(chunk.offset + 1) >= 0
}

其实是通过判断 checkChunkUploadedByResponse 的返回值来决定一个文件中的哪些块是已经上传了的,至于说判断规则的话完全可以和后端约定,自定义,这里模拟的就是说后端返回了uploaded_chunks数组,数组项是已经上传成功的 chunkNumber 集合,然后判断当前 chunk 是否在这个集合中;如果在,那么说明这个块已经上传过了,如果不在,说明还没上传过,需要上传。

这里的这个判断其实不只是对 test 的场景有用,即使不是test的情况,也一样可以处理,因为 checkChunkUploadedByResponse 的处理实际是在一个块请求成功后(可以是test的get请求,也可以是post真正上传的请求)判断的。

关于第2点,利用 checkChunkUploadedByResponse 的话,服务端可以直接返回 [1, 3, 4] ,然后只需要真正上传 2 这个块就好了。

@xyhxyw 能否满足需求呢?如果可以的话,我就加下文档,然后发一个新的版本

@dolymood ,恩,可以满足的,思路本身是对的,可以添加文档。我周末做下测试,然后再回复你。
非常感谢作者,问题处理非常快速!

0.2.0

checkChunkUploadedByResponse这个功能有一处感觉不太合适,对于第一个chunk,尽管函数判断的过程中返回了false,在testChunk这个http请求的状态码为200的情况下,不会上传第一个chunk。比较理想的情况是,服务端返回未上传的分片编号,前端根据函数判断的返回值判断是否需要上传此分片,而不是再根据http请求的状态码来判断

有道理,这个可以在下个版本来根据返回值直接决定需不需要,而非采用状态码,但是是个breaking change,需要发个大版本。#38

嗯,期待下一个大版本

关于 1 的 bug 似乎还没有解决,我在用分段上传了一个名称为car的文件夹里面有7张图片的时候,上传完成后,我再此点上传同一个文件夹,这时文件夹里面的文件会继续加入到car这个文件夹里,没上传一个 chunk,就请求一次合并,虽然我判断了同名文件夹,然后return false,但是fileList继续加入7张图片,一共14张

1的bug仍然存在,如果是第一次上传返回false的文件,则不进入队列。但是如果队列中有文件,这时再上传返回false的文件,虽然不会上传, 但是会在文件列表中展示

我们看下 @lubanproj 可以帮忙一起看下

@xyhxyw 不是思路,看这里:

checkChunkUploadedByResponse: function (chunk, message) {
var objMessage = {}
try {
objMessage = JSON.parse(message)
} catch (e) {}
// fake response
// objMessage.uploaded_chunks = [2, 3, 4, 5, 6, 8, 10, 11, 12, 13, 17, 20, 21]
// check the chunk is uploaded
return (objMessage.uploaded_chunks || []).indexOf(chunk.offset + 1) >= 0
}

其实是通过判断 checkChunkUploadedByResponse 的返回值来决定一个文件中的哪些块是已经上传了的,至于说判断规则的话完全可以和后端约定,自定义,这里模拟的就是说后端返回了uploaded_chunks数组,数组项是已经上传成功的 chunkNumber 集合,然后判断当前 chunk 是否在这个集合中;如果在,那么说明这个块已经上传过了,如果不在,说明还没上传过,需要上传。

这里的这个判断其实不只是对 test 的场景有用,即使不是test的情况,也一样可以处理,因为 checkChunkUploadedByResponse 的处理实际是在一个块请求成功后(可以是test的get请求,也可以是post真正上传的请求)判断的。

关于第2点,利用 checkChunkUploadedByResponse 的话,服务端可以直接返回 [1, 3, 4] ,然后只需要真正上传 2 这个块就好了。

能否考虑加一个test url自定义,或者让 checkChunkUploadedByResponse 纯粹跳过不用考虑有无test 或者第一个块已经上传成功。(预检查独立出来)
例如前端直传minio,在请求后端接口拿分片预上传地址的时候就可以知道哪些分片已经存在了。这种情况下

  1. target 是minio提供的、每个分片不同的上传地址,不能get获取到当前分片是否已经上传(如果这句有问题我回去拷打后端)
  2. 因为不能get,所以test是false,即便是true也只能得到 403 的结果
  3. checkChunkUploadedByResponse 在 2 的情况下得不到first success 的结果,只能通过上传第一片chunk结束之后走_updateUploadedChunks 才能去执行哪些跳过
  4. 造成的结果就是断点续传的时候第一片永远要传一次,后面的片才走跳过与否的判断
    如有错误还望海涵

——————————————————————————————————————————————

20240428 更新。问题已解决,通过target 的 mode 区分开测试路径和上传路径就行。