前端大文件上传

133 阅读3分钟

前端大文件上传

大文件上传基本思路

  • 将大文件转换成二进制流的格式(当你通过 <input type="file"> 元素选择文件时,浏览器已经为你提供了一个 File 对象,这个对象本质上就是一个包含文件内容的二进制大对象(Blob),这一步就省略了)。
  • 利用文件 Blob 原型上的 slice 方法进行切割,将二进制流切割成多份,将得到的切片数 组 chunkList 添加一些信息,比如文件名和下标,得到 uploadChunkList
  • 组装和分割块同等数量的请求块,并行或串行的形式发出请求
  • 待我们监听到所有请求都成功发出去以后,再给服务端发出一个合并的信号

<template>
  <h1>大文件上传</h1>
  <input type="file" @change="handleFileChange" />
  <el-button @click="handleUpload">上传</el-button>
</template>
<script>
const SIZE = 3 * 1024 * 1024; // 定义切片的大小
export default {
  data() {
    return {
      file: null, // 文件
      hash: '', // 文件的hash
      chunkList: [], // 切片列表
    };
  },
  methods: {
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) {
        this.file = null;
        return;
      }
      this.file = file;
    },
    // 生成文件切片
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        // file.slice 返回一个 blob对象
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    // 上传文件切片
    async uploadChunks(uploadedList = []) {
      // 构造请求列表
      const requestList = this.chunkList
        .map(({ chunk, chunkHash, index, fileHash }) => {
          const formData = new FormData();
          formData.append('chunk', chunk);
          formData.append('chunkHash', chunkHash);
          formData.append('fileHash', fileHash);
          return { formData, index };
        })
        .map(async ({ formData, index }) =>
          this.request({
            url: 'http://localhost:8080/upload-chunk',
            method: 'post',
            data: formData,
          })
        );
      await Promise.all(requestList); // 并发切片
      await this.mergeRequest(); // 合并切片
    },
    // 通知服务的合并切片
    async mergeRequest() {
      await this.request({
        url: 'http://localhost:8080/merge',
        method: 'post',
        headers: { 'content-type': 'application/json' },
        data: JSON.stringify({ filename: this.file.name, fileSize: this.file.size, size: SIZE, hash: this.hash }),
      });
    },
    //  上传按钮点击事件
    async handleUpload() {
      if (!this.file) {
        console.log('请选择一个文件吧');
        return;
      }
      // 文件分片
      const fileChunkList = this.createFileChunk(this.file);
      // 计算文件hash
      this.hash = await this.calculateHash(fileChunkList);
      // 构建 chunkList  添加下标以及 上传进度(是每一个chunk的上传进度)
      this.chunkList = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        size: file.size,
        chunkHash: `${this.hash}-${index}`,
        fileHash: this.hash,
        index,
        percentage: uploadedList.includes(`${this.hash}-${index}`) ? 100 : 0,
      }));
      // 上传 chunk
      await this.uploadChunks(uploadedList);
    },
  },
};
</script>


秒传

秒传指的是已经上传过的文件,不必重复传输,可以直接从后台取回文件资源。 前端实现思路如下:

  • 用户选择要上传的文件
  • 计算文件的hash值,利用hash值判断该文件是否已经存在
  • 如果已经存在,直接从后台获取该文件资源,上传结束
  • 如果不存在,则走正常的分片上传流程

断点续传

  • 为每一个文件切割块添加不同的标识
  • 当上传成功的之后,记录上传成功的标识
  • 当我们暂停或者发送失败后,可以重新发送没有上传成功的切割文件

进度条

xhr中提供了上传进度的事件progress,如果项目中使用的是axios ,axios在配置时提供了onUploadProgress监听原生progress事件。

var xhr = new XMLHttpRequest();  
  
// 监听上传进度  
xhr.upload.onprogress = function(event) {  
    if (event.lengthComputable) {  
        var percentComplete = (event.loaded / event.total) * 100;  
        console.log('Upload progress: ' + percentComplete.toFixed(2) + '%');  
    }  
};  
  
// 监听下载进度  
xhr.onprogress = function(event) {  
    if (event.lengthComputable) {  
        var percentComplete = (event.loaded / event.total) * 100;  
        console.log('Download progress: ' + percentComplete.toFixed(2) + '%');  
    }  
};  

后端

  • 接收每一个切割文件,并在接收成功后,存到指定位置,并告诉前端接收成功

  • 收到合并信号,将所有的切割文件排序,合并,生成最终的大文件,然后删除切割小文件,并告知前端大文件的地址