文件(分片)并行上传时计算总的上传进度

发布于:2025-05-11 ⋅ 阅读:(23) ⋅ 点赞:(0)

1. 基于已上传字节数计算

<template>
  <input type="file" multiple @change="uploadFiles" />
  <div>总进度:{{ totalProgress }}%</div>
</template>

<script>
export default {
  data() {
    return {
      totalProgress: 0,
      filesProgress: {}, // 存储每个文件的上传进度 { filename: percentage }
    }
  },
  methods: {
    async uploadFiles(e) {
      const files = e.target.files;
      if (!files.length) return;

      // 初始化进度记录
      this.filesProgress = {};
      Array.from(files).forEach(file => {
        this.filesProgress[file.name] = 0;
      });

      // 并行上传
      const promises = Array.from(files).map(file => {
        const formData = new FormData();
        formData.append('files', file);

        return axios.post('/upload', formData, {
          onUploadProgress: (progressEvent) => {
            // 更新单个文件进度
            this.filesProgress[file.name] = Math.round(
              (progressEvent.loaded / progressEvent.total) * 100
            );
            
            // 计算总进度
            this.calculateTotalProgress();
          }
        });
      });

      await Promise.all(promises);
    },
    calculateTotalProgress() {
      const progresses = Object.values(this.filesProgress);
      const total = progresses.reduce((sum, p) => sum + p, 0);
      this.totalProgress = Math.round(total / progresses.length);
    }
  }
}
</script>

原理:
为每个文件维护独立的进度(0-100)
实时计算所有文件进度的平均值

2. 基于已上传文件数计算(适合等大小文件)

data() {
  return {
    uploadedCount: 0,
    totalFiles: 0
  }
},
methods: {
  async uploadFiles(e) {
    const files = e.target.files;
    this.totalFiles = files.length;
    this.uploadedCount = 0;

    await Promise.all(files.map(file => {
      return axios.post('/upload', file)
        .then(() => {
          this.uploadedCount++;
          this.totalProgress = Math.round(
            (this.uploadedCount / this.totalFiles) * 100
          );
        });
    }));
  }
}

适用场景:当所有文件大小相近时更高效,实际计算的上传文件数占总数的比

3. 精确字节级计算

data() {
  return {
    totalSize: 0,       总字节数
    prevLoadedMap: {}, // 存储 { 文件名: 上次loaded值 }
    uploadedSize: 0,   // 总已上传字节数
  }
},
methods: {
getPrevLoaded(filename) {
    return this.prevLoadedMap[filename] || 0;
 },
 savePrevLoaded(filename, loaded) {
    this.prevLoadedMap[filename] = loaded;
 },
  async uploadFiles(e) {
    const files = e.target.files;
    
    // 计算总大小
    this.totalSize = Array.from(files).reduce((sum, file) => sum + file.size, 0);
    this.uploadedSize = 0;

    const promises = files.map(file => {
      const formData = new FormData();
      formData.append('file', file);

      return axios.post('/upload', formData, {
        onUploadProgress: (progressEvent) => {
          // 累加已上传字节数
          this.uploadedSize += progressEvent.loaded - this.getPrevLoaded(file.name);
          this.savePrevLoaded(file.name, progressEvent.loaded);
          
          this.totalProgress = Math.round(
            (this.uploadedSize / this.totalSize) * 100
          );
        }
      });
    });

    await Promise.all(promises);
  }
}
  1. progressEvent.loaded 的特性
    progressEvent.loaded 表示 当前文件已上传的字节数。
    在文件上传过程中,这个值会通过多次回调递增(例如从 0 → 500KB → 1MB → 完整文件大小)。
    问题:如果直接累加 progressEvent.loaded,会导致重复计算(例如第一次+500KB,第二次+1MB,实际只新增了500KB,但会错误累加1MB)。
  2. 解决重复累加的关键
    this.getPrevLoaded(file.name)
    获取该文件上一次回调时的 loaded 值(初始为0)。
    progressEvent.loaded - this.getPrevLoaded(file.name)
    计算出本次回调新增的字节数(正确的增量)。
    this.savePrevLoaded(file.name, progressEvent.loaded)
    保存当前 loaded 值,供下一次回调使用。

需要记录每个文件上次已上传的字节数
适用于文件大小差异大的场景