Angular文件分片上传

752 阅读2分钟

跟踪显示上传进度

要想发出一个带有进度事件的请求,你可以创建一个 HttpRequest 实例,并把 reportProgress 选项设置为 true 来启用对进度事件的跟踪。

    const req = new HttpRequest('POST', '/upload/file', file, {
      reportProgress: true 
    });

具体操作可以在官网看到angular.cn/guide/http#…

生成文件hash值

[SparkMD5]  www.npmjs.com/package/spa… 计算文件hash值

import * as SparkMD5  from 'spark-md5';
 
/**
   * Generate file hash
   *
   * @param fileChunkList 分片文件列表
   * @param callback 回调函数
   */
   calculateHash(fileChunkList, callback) {
    const spark = new SparkMD5.ArrayBuffer();
    let count = 0;
    const loadNext = index => {
      const reader = new FileReader();
      reader.readAsArrayBuffer(fileChunkList[index].file);
      reader.onload = e => {
        count++;
        spark.append(e.target.result);
        if (count === fileChunkList.length) {
          const hash = spark.end();
          callback(hash); //回调函数
        } else {
          loadNext(count);
        }
      };
    };
    loadNext(0);
  }

完整的文件上传代码:

import * as SparkMD5  from 'spark-md5';

const SIZE = 10 * 1024 * 1024; // file slice size
const concurrentNumber = 4 ;// Number of concurrent requests
private reUploadOptions = new Map<string, any>();

// 文件分片上传  
// option 配置参数
// 
sliceUpload(option, file, data) {
    if (!file) {
      return;
    }
    const fileChunkList = this.createFileChunk(file);
    const key = data[option.attrKey];// 一般是上传的对象的id
    const options = {fileChunkList, file, data, key};
    try{
      this.calculateHash(options, this.mergeRequest);
    } catch(e){
      // Don't work on the browser that is not Google Chrome
      // can not find file
      this.handleFileError(options, file.size);
      return;
    }
    // 成功的上传的index集合 
    options.successIndexes = [];
    
    this.handleUpload(options);
  }

  handleUpload(options){
    const fileChunks = this.chunkFile(options);

    this.concurrentUpload(options, fileChunks, 0);

  }


  // 根据concurrentNumber 区分一次上传的请求数 并预处理一下fileChunkList
  private chunkFile(options) {
    const fileChunkList = [];
    // Exclude already uploaded fileChunk
    _.forEach(options.fileChunkList,(fileChunk, index) => {
      if (!_.includes(options.successIndexes, index + 1)) {
        const data ={
          index,
          chunk: fileChunk.file,
          size: fileChunk.file.size
        };
        fileChunkList.push(data);
      }
    });

    return _.chunk(fileChunkList, concurrentNumber) ;
  }
   /**
   * Generate file slices
   *
   * @param file
   * @param size
   */
   private createFileChunk(file, size = SIZE) {
    const fileChunkList = [];
    let cur = 0;
    while (cur < file.size) {
      fileChunkList.push({file: file.slice(cur, cur + size)});
      cur += size;
    }
    return fileChunkList;
  }
   /**
   * Generate file hash
   *
   * @param options
   * @param callback
   */
   private calculateHash(options, callback) {
    const spark = new SparkMD5.ArrayBuffer();
    let count = 0;
    const loadNext = index => {
      const reader = new FileReader();
      reader.readAsArrayBuffer(options.fileChunkList[index].file);
      reader.onload = e => {
        count++;
        spark.append(e.target.result);
        if (count === options.fileChunkList.length) {
          options.data.hash = spark.end();
          callback(options);
        } else {
          loadNext(count);
        }
      };
    };
    loadNext(0);
  }
  
  // Concurrent request
  // Sending all requests together will block next other request.
  private concurrentUpload(options, fileChunks, index) {
    const uploadRequests = [];
    // preprocess request body
    _.forEach(fileChunks[index], data => {

      const url = this.utilService.constructUrlWithOptions(options.uploadUrl, {...data, ...options.data});
      const formData = new FormData();
      formData.append(options.fileAttr, data.chunk);
      uploadRequests.push(this.httpService.post(url,formData).pipe(
        map(()=> data.index),
        catchError(() =>  of(null))
      ));
    });
    let count = 0;
    const uploadSubscription = merge(...uploadRequests).subscribe( (res: number) => {

      if(res !== null) {
        count++;
        options.successIndexes.push(res + 1);
        //计算上传进度
        const progress = Math.round(options.successIndexes.length / options.fileChunkList.length * 100);
        ... //显示进度
      } else {
        //上传失败
        if(!options.file.size) { // 本地文件被修改报错
          // Don't work on the browser that is not Google Chrome
          this.handleFileError(options, options.file.size);
        } else {
          uploadSubscription.unsubscribe();
          // Handle error
          this.reUploadOptions.set(options.key, options); // 保存下失败的数据,方便文件续传
        }
      }
      // All success
      if (options.successIndexes.length === options.fileChunkList.length) {
        this.mergeRequest(options);
      } else if (count === fileChunks[index].length) {
        uploadSubscription.unsubscribe();
        this.concurrentUpload(options, fileChunks, index+1);
      }
    });
  }

  /**
   * Notify the server to merge slices
   *
   * @param options
   */
  private mergeRequest(options) {
    // 等所有文件都上传成功后并且hash计算完成,通知服务器
    if (options.successIndexes.length !== options.fileChunkList.length
      || !options.data.hash) {
      return;
    }
    this.mergeFile(options);
  }

文件续传

  resumeBreakpoint(key) {
    const options = this.reUploadOptions.get(key);
    if (options) {
      if (!options.data.hash) {
        this.calculateHash(options, this.mergeRequest);
      }
      const progress = Math.round(options.successIndexes.length / options.fileChunkList.length * 100);
        ... //显示进度
      this.handleUpload(options);
    }
  }

注意

js上传文件,不能刷新浏览器或退出浏览器。 最好在上传时给用户提示。