「从零开始的大文件上传 」都 2022 年了我怎么还在写这个话题...(二)

简介: 「从零开始的大文件上传 」都 2022 年了我怎么还在写这个话题...(二)

并发控制


这里之前的 unUploadFileListxhrMap 就派上了用场:


网络异常,图片无法展示
|


是否可以发起请求


还有待上传的文件并且 xhrMap 中正在上传的请求数小于最大并发数


unUploadFileList.length > 0 && xhrMap.size < concurrency


是否结束上传


没有待上传的文件并且 xhrMap 中没有正在进行的请求


isUploaded() {
  return this.unUploadFileList.length === 0 && this.xhrMap.size === 0;
}


完整代码


upload() {
  const { concurrency, xhrMap, unUploadFileList, xhrOptions, fileState } = this;
  return new Promise(resolve => {
    const run = async () => {
      while (unUploadFileList.length > 0 && xhrMap.size < concurrency) {
        const file = unUploadFileList.shift();
        const { xhr, request } = this.requestFactory(file, xhrOptions, this.progressFactory(file));
        xhrMap.set(file, xhr);
        request.finally(()=> {
          xhrMap.delete(file);
          if (this.isUploaded()) {
            resolve();
            this.progressEvent('END', {
              fileState
            });
          } else {
            run();
          }
        });
      }
    };
    run();
  });
}


取消上传


取消上传分为取消单个文件的上传和取消全部未完全上传的文件,这里用到的就是 abort 这个 api,他的语法为:


xhrInstance.abort();


如果该请求已被发出,XMLHttpRequest.abort() 方法将终止该请求。当一个请求被终止,它的  readyState 将被置为 XMLHttpRequest.UNSENT(0),并且请求的 status 置为 0


取消上传的逻辑就是如果该请求在 xhrMap 里就 abort 掉,如果在 unUploadFileList 里就直接把它从数组里移除。


abort(file) {
  if (this.xhrMap.has(file)) {
    this.xhrMap.get(file)?.abort();
  } else {
    const fileIndex = this.unUploadFileList.indexOf(file);
    this.unUploadFileList.splice(fileIndex, 1);
  }
}
abortAll() {
  this.xhrMap.forEach(xhr => xhr?.abort());
  this.unUploadFileList = [];
}


重新上传


当文件上传失败了,我们可以重新上传,这个就很简单了,就是把 file 添加到 unUploadFileList 中,当然,重新上传时有可能已经 upload 结束,此时就重新触发 upload


redoFile(file) {
    this.fileState.failCount--;
    this.unUploadFileList.push(file);
    if (this.isUploaded()) {
      this.upload();
    }
}


完整代码


class ZetaUploader {
  fileState = {
    failCount: 0,
    successCount: 0,
    abortCount: 0
  }
  /* progressEvent arguments
  * @params {Object} [params]
  * @params {ENUM} [params?.state] FAIL PROGRESS SUCCESS END
  * @params {Info} [params?.info]
  * @params {File} [info?.file]
  * @params {Number} [info?.progress]
  * @params {FileState} [info?.fileState]
  */
  progressEvent = () => { };
  concurrency = 1;
  xhrMap = null;
  unUploadFileList = [];
  /*
  * @params {Object} [xhrOptions]
  * @params {String} [params.url]
  * @params {String} [params.method]
  * @params {Object} [params.headers]
  * @params {Function} [params.getXhrDataByFile]
  */
  xhrOptions = null; 
  static isRequestSuccess(progressEvent) {
    return String(progressEvent.target.status).startsWith('2');
  }
  constructor(progressEvent, fileList, concurrency, xhrOptions) {
    const basicXhrOptions = {
      url: '',
      method: 'post',
      headers: [],
      getXhrDataByFile: file => {
        const formData = new FormData();
        formData.append('file', file);
        return formData;
      }
    };
    // BASIC ATTRS
    this.progressEvent = progressEvent;
    this.concurrency = concurrency;
    this.xhrOptions = Object.assign(basicXhrOptions, xhrOptions);
    // COMPUTED ATTRS
    this.unUploadFileList = fileList;
    this.xhrMap = new Map();
  }
}
class BasicZetaUploader extends ZetaUploader {
  constructor(progressEvent, fileList, concurrency, xhrOptions) {
    super(progressEvent, fileList, concurrency, xhrOptions);
  }
  progressFactory(file) {
    return e => {
      this.progressEvent('PROGRESS', {
        file,
        progress: parseInt(String((e.loaded / e.total) * 100))
      });
    };
  }
  /*
  * @params {File} [file]
  * @params {xhrOptions} [xhrOptions]
  * @params {Function} [onProgress]
  */
  requestFactory(file, xhrOptions, onProgress) {
    const { url, method, headers, getXhrDataByFile } = xhrOptions;
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
    let _resolve = () => { };
    let _reject = () => { };
    xhr.onprogress = onProgress;
    xhr.onload = e => {
      // 需要加入response的判断
      if (ZetaUploader.isRequestSuccess(e)) {
        this.fileState.successCount++;
        this.progressEvent('SUCCESS', {
          file
        });
        _resolve({
          data: e.target.response
        });
      } else {
        this.fileState.failCount++;
        this.progressEvent('FAIL', {
          file
        });
        _reject();
      }
    };
    xhr.onerror = () => {
      this.fileState.failCount++;
      this.progressEvent('FAIL', {
        file
      });
      _reject();
    };
    xhr.onabort = () => {
      this.fileState.abortCount++;
      _resolve({
        data: null
      });
    };
    const request = new Promise((resolve, reject) => {
      _resolve = resolve;
      _reject = reject;
      xhr.send(getXhrDataByFile(file));
    });
    return {
      xhr,
      request
    };
  }
  upload() {
    const { concurrency, xhrMap, unUploadFileList, xhrOptions, fileState } = this;
    return new Promise(resolve => {
      const run = async () => {
        while (unUploadFileList.length > 0 && xhrMap.size < concurrency) {
          const file = unUploadFileList.shift();
          const { xhr, request } = this.requestFactory(file, xhrOptions, this.progressFactory(file));
          xhrMap.set(file, xhr);
          request.finally(()=> {
            xhrMap.delete(file);
            if (this.isUploaded()) {
              resolve();
              this.progressEvent('END', {
                fileState
              });
            } else {
              run();
            }
          });
        }
      };
      run();
    });
  }
  isUploaded() {
    return this.unUploadFileList.length === 0 && this.xhrMap.size === 0;
  }
  abort(file) {
    if (this.xhrMap.has(file)) {
      this.xhrMap.get(file)?.abort();
    } else {
      const fileIndex = this.unUploadFileList.indexOf(file);
      this.unUploadFileList.splice(fileIndex, 1);
    }
  }
  abortAll() {
    this.xhrMap.forEach(xhr => xhr?.abort());
    this.unUploadFileList = [];
  }
  redoFile(file) {
    this.fileState.failCount--;
    this.unUploadFileList.push(file);
    if (this.isUploaded()) {
      this.upload();
    }
  }
}
export {
  BasicZetaUploader
};


结束语


网络异常,图片无法展示
|


本篇文章到这里就结束了,「本系列的」下一篇文章不出意外的话会带着大家了解文件分片与断点续传的原理和实现思路。


最后,下一篇文章见~


✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

我生于长空,长于烈日;
我翱翔于风,从未远去。

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

相关文章
|
3天前
|
网络安全 网络性能优化 数据中心
想要丝滑地使用ACL,少不了这篇干货~
想要丝滑地使用ACL,少不了这篇干货~
答知识星球朋友疑问:执行 ABAP 代码出现超时的原因,背后的理论和解决方案试读版
答知识星球朋友疑问:执行 ABAP 代码出现超时的原因,背后的理论和解决方案试读版
|
10月前
|
JavaScript
🚀VuePress-theme-hope2 搭建个人网站,万字长文保姆级教程,包含自动部署、评论、搜索等功能1
🚀VuePress-theme-hope2 搭建个人网站,万字长文保姆级教程,包含自动部署、评论、搜索等功能
|
10月前
|
程序员
🚀VuePress-theme-hope2 搭建个人网站,万字长文保姆级教程,包含自动部署、评论、搜索等功能2
🚀VuePress-theme-hope2 搭建个人网站,万字长文保姆级教程,包含自动部署、评论、搜索等功能
|
缓存 Kubernetes 算法
公开下载 | 300+页《服务端开发与面试知识手册》,12.8w字经典架构知识
公开下载 | 300+页《服务端开发与面试知识手册》,12.8w字经典架构知识
628 0
|
缓存 自然语言处理 小程序
这个迭代写了个小程序,顺便整理了一份笔记 📒 (4000字)
这个迭代写了个小程序,顺便整理了一份笔记 📒 (4000字)
155 0
|
C++ UED
开心档之开发入门网-C++ 有用的资源
开心档之开发入门网-C++ 有用的资源
|
传感器
时隔这么长时间,我把常用的功能整理好了,再来感受VueUse工具库的优雅吧~
时隔这么长时间,我把常用的功能整理好了,再来感受VueUse工具库的优雅吧~
时隔这么长时间,我把常用的功能整理好了,再来感受VueUse工具库的优雅吧~
|
C++
爱上c++的第十二天:文件流的概念(详细版本)
c++的程序运行是要以类对象为操作单位的,要以磁盘文件为对象进行输入输出时,必须定义一个文件流的对象,通过文件流对象将数据从内存输出到磁盘文件,或者通过文件流对象从磁盘文件将数据输入到内存。
100 0
|
自然语言处理 前端开发 调度
「从零开始的大文件上传 」都 2022 年了我怎么还在写这个话题...(一)
「从零开始的大文件上传 」都 2022 年了我怎么还在写这个话题...(一)
221 0