一、index.ts

import Embitter from "../tools/emmitter";import Slice from "./sliceUpload";// 上传状态const statusMap = {  fail: 0, // 失败  success: 1, // 胜利  inProcessing: 2, // 进行中  paused: 3, // 暂停  canceled: 4, // 已勾销  wait: 5 // 未开始};interface IConfig {  file: File;  parallel: number;  partSize: number;}class Client extends Embitter {  private file: File;  private parallel: number; // 分片并行数量  private partSize: number; // 分片大小  private status: number; // 0 失败 1胜利 2进行中 3暂停 4勾销 5未开始 6报错  private partInfo: IPartInfo;  private partsArray: Slice[]; // 保护所有分片  private sliceUploadSuccess: Set<number>; // 上传胜利的分片partIndex  private sliceUploadRunning: Set<number>; // 上传中的分片partIndex  private sliceEvent: Embitter;  constructor(config: IConfig) {    super();    this.file = config.file;    this.parallel = config.parallel || defaultParallel;    this.partInfo = {      partSize: config.partSize,      partNum: Math.ceil(config.file.size / config.partSize),    };    this.status = statusMap.wait; // 默认未开始    this.partsArray = [];    // 记录胜利的分片上传    this.sliceUploadSuccess = new Set([]);    // 记录上传中的分片    this.sliceUploadRunning = new Set([]);    // 监听分片上传    this.sliceEvent = new Embitter();  }  getConfig() {    return {      file: this.file,      partInfo: this.partInfo,      sliceEvent: this.sliceEvent,      status: this.status,    };  }  private startUploadAllSlice() {    this.sliceEvent.on("success", (partIndex: number) => {      if (!this.sliceUploadSuccess.has(partIndex)) {        this.sliceUploadSuccess.add(partIndex);        this.sliceUploadRunning.delete(partIndex);        const progress = this.sliceUploadSuccess.size / this.partInfo.partNum;        this.emmit("progress", progress);        if (progress >= 1) {          this.endSliceUpload();        } else {          this._resumeUpload();        }      }    });    this.sliceEvent.on("error", (partIndex: number, err: any) => {      this.emmit("error", err);      this.sliceUploadRunning.delete(partIndex);      this.status = statusMap.paused; // 上传出现异常就先暂停上传      this.emmit("pause");    });    const that = this;    // 创立分片上传实例    this.partsArray = Array.from(      new Array(this.partInfo.partNum),      (x, i) =>        new Slice({          client: that,          partIndex: i + 1,        })    );    // 开始分片上传    this._resumeUpload();  }  /**   * 上传文件:开始上传或者复原上传   * @param file   * @returns {Promise<void>}   */  private async uploadFile(): Promise<void> {    if (this.status !== statusMap.inProcessing) {      this.status = statusMap.inProcessing;      this.emmit("start");    }    // 开始上传    this.startUploadAllSlice();  }  /**   * 开始上传   * @param file   * @returns {Promise<void>}   */  public async startUpload(): Promise<void> {    if (this.status !== statusMap.wait) {      console.log("startUpload status error", this.status);      this.emmit("error", errorMap.hasStarted);      return;    }    this.uploadFile();  }  // 完结分片上传事务  private async endSliceUpload() {    if (statusMap.canceled === this.status) {      return;    }    const tagList = [];    for (const val of this.partsArray) {      tagList.push({        partIndex: val.partIndex,        partTag: val.partTag,      });    }    // 完结分片上传事务    const res = await action.endMultipartUpload({tagList});    if (!res.hasOwnProperty("err")) {      this.status = statusMap.success;      this.emmit("success", {        resourceId: res.resourceId,        preResourceId: res.preResourceId,        url: res.path,      });    }  }  /**   * 暂停上传   * @returns {Promise<void>}   */  public async pauseUpload() {    // 只有文件正在上传中,才能够暂停    if (this.status !== statusMap.inProcessing) {      this.emmit("error", errorMap.noInProcess);      return;    }    this.status = statusMap.paused;    this.emmit("pause");  }  /**   * 分片上传开始   * @returns   */  private _resumeUpload() {    const todoParts = this.partsArray.filter(      (part) =>        !this.sliceUploadSuccess.has(part.partIndex) &&        !this.sliceUploadRunning.has(part.partIndex)    );    let job;    while (      this.sliceUploadRunning.size < this.parallel &&      ![statusMap.paused, statusMap.canceled].includes(this.status) &&      (job = todoParts.shift())    ) {      this.sliceUploadRunning.add(job.partIndex);      job.startUpload();    }  }  /**   * 复原上传   * @returns {Promise<void>}   */  public async resumeUpload() {    if (this.status !== statusMap.paused) {      this.emmit("error", errorMap.noCanResume);      return;    }    this.status = statusMap.inProcessing;    this.emmit("start");    this._resumeUpload();  }  /**   * 破除上传   * @returns {Promise<void>}   */  public async abortUpload(): Promise<void> {    // 只有文件正在上传中或者上传曾经暂停,才能够勾销上传    if (      this.status !== statusMap.inProcessing &&      this.status !== statusMap.paused    ) {      this.emmit("error", errorMap.noCanCancel);      return;    }    const result = await action.abortUpload();    // 勾销上传胜利    if (!result.err) {      this.status = statusMap.canceled;      this.emmit("abort");      this.emmit("progress", 0);      return;    }    this.emmit("error", result.err);  }}export default Client;

二、sliceUpload.ts

import { generateMD5 } from "../tools/utils";import action from "../action";import Client from "./index";class Slice {  // 以下是独有属性  private sliceFile: Blob;  private md5Content: string;  private signatureUrl: string;  private _status: number; // 0 失败 1胜利 2进行中 3暂停 4勾销 5未开始 6报错  private _partTag: string; // 上传胜利会有标签  private _partIndex: number;  private client: Client; // 上传实例  constructor(config: ISliceConfig) {    this.client = config.client;    this._partIndex = config.partIndex;    this._status = statusMap.wait;    this._partTag = "";  }  get partTag() {    return this._partTag;  }  get status() {    return this._status;  }  get partIndex() {    return this._partIndex;  }  /**   * 获取分片签名上传url   */  private async getSignatureUrl() {    const { file, sliceEvent } = this.client.getConfig();    const res = await action.getMultipartUrl(this._partIndex, {      headers: {        "FileContent-MD5": this.md5Content,        "FileContent-Type": file.type || "application/octet-stream",      },    });    if (!res.err) {      this.signatureUrl = res.signatureUrl;    } else {      if (this.getCurrentStatus() === statusMap.canceled) {        return;      }      this._status = statusMap.fail;      sliceEvent.emmit("error", this.partIndex, res.err);    }  }  private generateSliceFile() {    const { partInfo, file } = this.client.getConfig();    const start = (this._partIndex - 1) * partInfo.partSize;    let end = start + partInfo.partSize;    if (this._partIndex === partInfo.partNum) {      end = file.size;    }    this.sliceFile = file.slice(start, end);  }  private isContinueUpload() {    const status = this.getCurrentStatus();    return ![statusMap.canceled, statusMap.paused].includes(status);  }  private getCurrentStatus() {    const { status } = this.client.getConfig();    return status;  }  private async uploadSlice() {    const { file, sliceEvent } = this.client.getConfig();    const res = await action.startPartUpload(      this.signatureUrl,      this.sliceFile,      {        headers: {          "Content-MD5": this.md5Content,          "Content-Type": file.type || "application/octet-stream",        },      }    );    if (!res.err) {      this._status = statusMap.success;      try {        this._partTag = JSON.parse(res.headers.etag);      } catch (error) {        this._partTag = res.headers.etag;      }      sliceEvent.emmit("success", this._partIndex);    } else {      if (this.getCurrentStatus() === statusMap.canceled) {        return;      }      this._status = statusMap.fail;      sliceEvent.emmit("error", this._partIndex, res.err);    }  }  // 上传分片入口  async startUpload() {    if (this.isContinueUpload() && !this.sliceFile) {      this.generateSliceFile();    }    if (this.isContinueUpload() && !this.md5Content && this.sliceFile) {      this.md5Content = await generateMD5(this.sliceFile);    }    if (this.isContinueUpload() && !this.signatureUrl && this.md5Content) {      await this.getSignatureUrl();    }    if (this.isContinueUpload() && this.signatureUrl) {      await this.uploadSlice();    }    return this._status === statusMap.success;  }}export default Slice;

三、demo 集成

import xyUpload from "./index.ts";const client = new xyUpload({  file,  parallel: 3,  partSize: 2000000,});client.on("progress", (res) => {  console.log("progress", res);});client.on("success", (res) => {  resolve(res);});client.on("error", (res) => {  reject(res);});client.on("start", () => {  console.log("开始了");});// 开始上传client.startUpload();// 暂停上传client.pauseUpload();// 持续上传client.resumeUpload();// 破除上传client.abortUpload();