【前端学习记录】记一次分片上传逻辑的调试过程

前言

在项目开发的过程中,经常会遇到上传和下载,对于上传来说,如果是小文件的话,接口响应会比较快,但是对于大文件,则需要对其分片以减少请求体的大小和上传时间。

小文件上传

以Vue框架使用为例,直接上代码

<template>
  <div>
    <el-upload
      class="upload-demo"
      action="your_upload_api_url"
      :on-success="handleSuccess"
      :before-upload="beforeUpload"
    >
      <el-button size="small" type="primary">点击上传</el-button>
      <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
    </el-upload>
  </div>
</template>

<script>
export default {
  methods: {
    handleSuccess(response, file) {
      // 处理上传成功的逻辑
      console.log(response, file);
    },
    beforeUpload(file) {
      // 在上传之前的操作,例如限制文件类型、大小等
      const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
      if (!isJPG) {
        this.$message.error('只能上传jpg/png文件');
      }
      const isLt500K = file.size / 1024 < 500;
      if (!isLt500K) {
        this.$message.error('文件大小不能超过500KB');
      }
      return isJPG && isLt500K;
    },
  },
};
</script>

<style scoped>
/* 样式可以根据自己的需求进行调整 */
.upload-demo {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 180px;
}
</style>

在上述代码中:

组件用于处理文件上传,通过 action 属性指定文件上传的接口。
:on-success 属性绑定一个方法,在文件上传成功后触发。
:before-upload 属性绑定一个方法,在文件上传之前触发,可以在该方法中进行一些操作,如限制文件类型和大小。
元素用于触发文件选择。
请注意替换 your_upload_api_url 为实际的文件上传接口。

分片上传

文件过大时就需要进行文件分片上传,文件分片上传是一种将大文件拆分成小块(分片)并分别上传的策略,这样可以更有效地处理大文件上传,避免一次性上传整个文件可能遇到的网络问题和服务器限制。FormData 对象和一些前端框架/库(如 axios)通常与文件分片上传一起使用。

下面是一个简单的实现示例,使用 FormData 和 axios 进行文件分片上传:

<template>
  <div>
    <input type="file" ref="fileInput" @change="handleFileChange" />
    <button @click="startUpload">开始上传</button>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      selectedFile: null,
      chunkSize: 1024 * 1024, // 每个分片的大小,这里设置为1MB
    };
  },
  methods: {
    handleFileChange(event) {
      this.selectedFile = event.target.files[0];
    },
    async startUpload() {
      if (!this.selectedFile) {
        alert('请选择文件');
        return;
      }

      // 计算总分片数量
      const totalChunks = Math.ceil(this.selectedFile.size / this.chunkSize);

      // 循环上传分片
      for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
        const start = chunkIndex * this.chunkSize;
        const end = Math.min(start + this.chunkSize, this.selectedFile.size);
        const chunk = this.selectedFile.slice(start, end);

        const formData = new FormData();
        formData.append('file', chunk);
        formData.append('chunkIndex', chunkIndex);
        formData.append('totalChunks', totalChunks);

        try {
          await axios.post('your_chunk_upload_api_url', formData);
          console.log(`分片 ${chunkIndex + 1} / ${totalChunks} 上传成功`);
        } catch (error) {
          console.error(`分片 ${chunkIndex + 1} / ${totalChunks} 上传失败`, error);
          // 处理上传失败的逻辑,可以选择中止上传或重试
          return;
        }
      }

      console.log('文件上传完成');
    },
  },
};
</script>

上面是一个分片上传的示例,在实际操作时遇到了一些问题

分片上传遇到的问题

问题1:请求体过大,如何处理?

项目中遇到的文件最大约1个GB,此时直接上传,会报请求体过大的报错,经过调试后发现文件最大传输为50MB。由于项目是依赖于平台,属于平台的子项目,因此在前后端联调时,前端通过nginx转发到对应的接口上。在nginx配置里,有50M大小的限制,修改后生效。后台同事在排查后台代码及配置也发现了请求不能过大的限制条件,即ingress中设置了请求的大小,最终两者同时修改后生效

nginx中的配置修改如下:
【前端学习记录】记一次分片上传逻辑的调试过程_第1张图片

问题2:前端如何获取上传进度条?

在使用 axios 进行文件上传时,你可以通过配置 onUploadProgress 属性来监听上传进度。onUploadProgress 允许你在上传过程中获取上传进度,并执行相应的操作。

axios.post('your_upload_api_url', formData, {
    onUploadProgress: progressEvent => {
      const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
      console.log(`上传进度: ${percentCompleted}%`);
      // 在这里可以更新进度条或执行其他操作
    },
  })
  .then(response => {
    // 处理上传成功的逻辑
    console.log(response.data);
  })
  .catch(error => {
    // 处理上传失败的逻辑
    console.error('上传失败', error);
  });

需要注意的是,这里的progressEvent.total并不一定和文件的大小相等,处理百分比时,尽量使用total而不是file.size。

问题3:串行上传时文件上传没有问题,但是并行上传时,后台取到的文件片组装后错误?

经过定位,发现是在分片时,最后一片会比较小,如果串行上传时,前端一片片按着顺序依次上传,想优化上传速度,使用并行上传时,最后一片通常都会比前面的分片小,导致最后一片先上传,然后发生组装错误。经过测试,可以采取以下思路:1、前端最后一片在前面的分片都上传完毕后,上传最后一片,可使用for循环配合Promise.all处理;2、后端在拿到所有的分片后再开始组装,而不是边上传边组装。

问题4:进度条上传达到100%后,没有立刻返回结果

这是因为后台接收到文件后,可能还有处理的时间,但是文件已经传到了后台,如果后台没有其他逻辑处理,可以直接返回结果,告知用户上传已完成
最后,放上去部分代码

async handleUpload() {
      const file = this.formData.file;
      // 初始化分片的大小,可以自定义
      const chunkSize = 200 * 1024 * 1024;

      // 计算分片的数量, 传参时会用到
      const chunkCount = Math.ceil(file.size / chunkSize);

      // 用于保存每个分片的信息
      const chunks = [];

      if (file.size > chunkSize) {
        // 分割文件为多个分片
        for(let i = 0; i < chunkCount; i++) {
          const start = i * chunkSize;
          const end = Math.min(file.size, (i + 1) * chunkSize);
          const chunk = file.raw.slice(start, end);
          chunks.push(chunk);
        }
      } else {
        chunks.push(file.raw);
      }

      try {
        this.uploadLoading = true;
        // 创建一个数组来存储每个分片上传的 Promise
        const uploadPromises = [];
        for(let i = 0; i < chunks.length; i++) {
          this.loadedSizeArr[i] = 0;
          this.totalSizeArr[i] = 0;
        }
        const fileFlag = getRandomName();
        let percentage = 1;
        if (chunks.length > 1) {
          percentage = (chunks.length-1) / chunks.length;
        }
        const headers = {
          fileFlag,
          fileName: file.name,
          'Content-Type': 'multipart/form-data',
        }
        // 遍历并上传每个分片
        for(let i = 0; i < Math.max(chunks.length - 1, 1); i++) {
          const formData = new FormData();
          formData.append('file', chunks[i]);
          formData.append('chunkNumber', String(i+1));
          formData.append('totalChunks', String(chunkCount));

          // 创建分片上传的 Promise
          const uploadPromise = util.post('your_upload_api_url', formData, {
            headers,
            onUploadProgress: (progressEvent) => {
              if (progressEvent.lengthComputable) {
                this.loadedSizeArr[i] = progressEvent.loaded;
                this.totalSizeArr[i] = progressEvent.total;
                const loadedSizeTotal = this.loadedSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
                const totalSizeTotal = this.totalSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
                this.percent = Math.round(loadedSizeTotal / totalSizeTotal * percentage * 100);
              }
            },
          })
          // 将 Promise 存储到数组中
          uploadPromises.push(uploadPromise);
        }
        // 使用 Promise.all 来等待所有分片上传完成
        Promise.all(uploadPromises)
          .then(async () => {
            if (chunkCount > 1) {
              const formData = new FormData();
              formData.append('file', chunks[chunks.length-1]);
              formData.append('chunkNumber', String(chunkCount));
              formData.append('totalChunks', String(chunkCount));
              await util.post('your_upload_api_url', formData, {
                headers,
                onUploadProgress: (progressEvent) => {
                  if (progressEvent.lengthComputable) {
                    this.loadedSizeArr[chunks.length-1] = progressEvent.loaded;
                    this.totalSizeArr[chunks.length-1] = progressEvent.total;
                    const loadedSizeTotal = this.loadedSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
                    const totalSizeTotal = this.totalSizeArr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
                    this.percent = Math.round((percentage + loadedSizeTotal / totalSizeTotal / chunkCount) *100);
                  }
                },
              })
            }

            // 所有分片上传完成后执行的逻辑
            this.percent = 100;
            this.uploadLoading = false;
            this.$notify({
              type: 'success',
              title: '成功',
              message:  '上传成功!',
            })
          })
      } catch (e) {
        this.$notify({
          type: 'error',
          title: '失败',
          message: '上传失败!',
        });
      }
    },

你可能感兴趣的:(前端,前端,学习)