「从零开始的大文件上传 」都 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
};


结束语


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


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


最后,下一篇文章见~


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

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

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

相关文章
|
6月前
|
数据采集 机器学习/深度学习 数据可视化
分享68个Python爬虫源码总有一个是你想要的
分享68个Python爬虫源码总有一个是你想要的
209 0
|
3月前
|
数据采集 前端开发 JavaScript
《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件
本文档详细介绍了从零开始搭建一个包含前后端交互的热搜展示项目的全过程。通过本教程,读者不仅能学习到完整的项目开发流程,还能掌握爬虫技术和前后端交互的具体实践。适合有一定编程基础并对项目实战感兴趣的开发者参考。
86 1
|
3月前
|
数据采集 Java 数据库连接
《花100块做个摸鱼小网站! 》第二篇—后端应用搭建和完成第一个爬虫
本文详细介绍了一个基于Spring Boot的后端应用搭建过程,包括Maven项目结构的规划与配置、依赖管理、环境变量配置、数据库连接配置等。作者通过实际案例——一个摸鱼小网站的开发,逐步引导读者理解并实践项目的搭建流程。此外,还分享了如何利用Postman从cURL命令快速生成HTTP请求代码的方法,并演示了如何将这些代码整合进项目中,实现了一个简单的定时爬取抖音热搜数据的功能。文章不仅提供了详尽的代码示例,还附带了丰富的截图说明,非常适合希望从零开始构建Web应用的开发者参考学习。
59 3
《花100块做个摸鱼小网站! 》第二篇—后端应用搭建和完成第一个爬虫
|
3月前
|
网络安全 网络性能优化 数据中心
想要丝滑地使用ACL,少不了这篇干货~
想要丝滑地使用ACL,少不了这篇干货~
|
3月前
|
C# 开发者 Windows
勇敢迈出第一步:手把手教你如何在WPF开源项目中贡献你的第一行代码,从选择项目到提交PR的全过程解析与实战技巧分享
【8月更文挑战第31天】本文指导您如何在Windows Presentation Foundation(WPF)相关的开源项目中贡献代码。无论您是初学者还是有经验的开发者,参与这类项目都能加深对WPF框架的理解并拓展职业履历。文章推荐了一些适合入门的项目如MvvmLight和MahApps.Metro,并详细介绍了从选择项目、设置开发环境到提交代码的全过程。通过具体示例,如添加按钮点击事件处理程序,帮助您迈出第一步。此外,还强调了提交Pull Request时保持专业沟通的重要性。参与开源不仅能提升技能,还能促进社区交流。
43 0
|
3月前
|
数据采集 存储 JavaScript
打造你的第一个网页爬虫:一步步走进数据抓取的世界
【8月更文挑战第31天】在数字信息泛滥的时代,能够快速获取并利用网络数据变得尤为重要。本文将带你从零开始,用Python构建一个简单的网页爬虫。我们会一起探索请求网页、解析内容、存储数据的全过程。准备好了吗?让我们揭开数据抓取的神秘面纱,开启一段代码探险之旅!
|
6月前
|
数据采集 数据可视化 Java
分享66个Python爬虫源码总有一个是你想要的
分享66个Python爬虫源码总有一个是你想要的
213 1
|
11月前
|
测试技术 Go
【测试平台系列】第一章 手撸压力机(七)- 使用gin
今天,我们使用gin框架将压力机做成一个web服务后端。 我们引入gin框架:
【测试平台系列】第一章 手撸压力机(七)- 使用gin
|
11月前
|
存储 JSON 搜索推荐
【测试平台系列】第一章 手撸压力机(十二)-初步实现提取功能
上一章节,我们主要实现了基础的并发测试场景的能力。本章节,我们实现一下,如何对响应进行提取,使用正则/json对响应信息提取,并赋值给我们定义的变量。
|
存储 数据库 数据安全/隐私保护
我拿回属于自己的数据,怎么了?|将印象笔记导入笔记软件Notion
我拿回属于自己的数据,怎么了?|将印象笔记导入笔记软件Notion