# vue2大文件切片上传,断点续传,秒传

223 阅读3分钟

上传文件遇到大文件的时候,非常影响上传速度,有时候会文件无响应,这时候需要用到文件上传,文件上传是一个常见的功能需求,原理通过将大文件切分成多个小块(切片)进行上传,不仅可以有效减少单次上传的数据量,降低网络波动对上传过程的影响,还能实现如断点续传、秒传等功能。

1.选择文件
2.计算文件的MD5值
3.校验文件是否需要上传(通过后端接口查询)
4.文件切片
5.并发上传切片 (浏览器最大请求数6个)
6.通知后端合并切片

第一步:选择文件

<template>
  <div>
    <input type="file" @change="handleFileChange" accept="video/*" />
  </div>
</template>
​
async handleFileChange(event){
   if (!event.target.files[0]) return; // 未选择文件则返回
},

第二步:计算文件 MD5

使用spark-md5 进行文件加密,使得同一个文件的MD5是一样的

//安装md5   npm i spark-md5
async computeFileHash(file) {
  const spark = new SparkMD5.ArrayBuffer();
  const fileReader = new FileReader();
  return new Promise((resolve) => {
    fileReader.onload = (e) => {
      spark.append(e.target.result);
      const hash = spark.end();
      resolve(hash);
    };
    fileReader.readAsArrayBuffer(file);
  });
}

第三步:校验文件是否需要上传(通过后端接口查询)

通过调后端查询接口,判断是否需要上传,不需要直接跳过(秒传)

checkFile(fileHash){
  //调用后端接口判断文件是否上传过
  //模拟接口 shouldUpload:是否应该上传,uploadedChunks:已经上传的切片
  return { shouldUpload: true, uploadedChunks: [] };
},

第四步:文件切片

根据接口返回值判断是否需要上传,需要上传的进行切片,并加入请求队列准备上传

 sliceFileAndUpload(fileHash, uploadedChunks){
  const chunkSize = 10 * 1024 * 1024; // 切片大小,这里是10MB
  this.chunkCount = Math.ceil(this.selectedFile.size / chunkSize); // 计算总切片数
  for (let i = 0; i < this.chunkCount; i++) {
    if (uploadedChunks.includes(i)) continue; // 跳过已上传的切片
    const chunk = this.selectedFile.slice(i * chunkSize, (i + 1) * chunkSize); // 获取切片
    this.requestPool.push({ chunk, index: i }); // 加入请求池
  }
  this.processPool(fileHash); // 开始处理请求池
},

第五步:并发上传切片

采用并发上传,控制上传的个数,上传完成一个切片后将取出一个切片放入请求队列中,实现最大化。

processPool(fileHash){
  while (this.requestPool.length > 0 && this.MAX_REQUEST > 0) {
    const { chunk, index } = this.requestPool.shift(); // 取出一个待上传的切片
    this.uploadChunk(chunk, fileHash, index) // 上传切片
      .then(() => {
        this.uploadedChunksCount++; // 更新已上传切片数量
        if (this.requestPool.length > 0) {
          this.processPool(fileHash); // 继续处理请求池
        } else if (this.uploadedChunksCount === this.chunkCount) {
          //TODO 所有切片都已上传,通知服务器合并
        }
      })
      .finally(() => {
        this.MAX_REQUEST++; // 释放一个请求槽
      });
    this.MAX_REQUEST--; // 占用一个请求槽
  }
},
    
 async uploadChunk(chunk, fileHash, index){
  const formData = new FormData();
  formData.append("chunk", chunk);
  formData.append("hash", fileHash);
  formData.append("index", index);
  // 替换为真实的上传URL
  // await axios.post("xxx", formData);
}    

第六步:通知后端合并切片

所以切片上传完成后,通知后端去合并切片,即一个切片上传完成。

mergeChunkFiles(fileHash){
  // axios.post("xxx", fileHash);
}

完整代码

<template>
  <div>
    <input type="file" @change="handleFileChange" accept="video/*" />
  </div>
</template>
​
<script>
import SparkMD5 from "spark-md5"; // 引入SparkMD5用于计算文件的MD5值
export default {
  name: 'upload',
  data(){
    return{
      selectedFile:null,//选中的文件
      chunkCount:0,//切片总数
      requestPool:[],//请求队列
      MAX_REQUEST:6,//最大并发量
      uploadedChunksCount: 0, // 已上传的切片数量
    }
  },
  methods:{
    async handleFileChange(event){
       if (!event.target.files[0]) return; // 未选择文件则返回
       this.selectedFile=event.target.files[0]
       const fileHash = await this.computeFileHash(event.target.files[0]); // 计算文件hash
      
       const { shouldUpload, uploadedChunks } = await this.checkFile(fileHash); // 检查文件是否需要上传
       if(!shouldUpload){
        console.log('秒传成功');
        return;
       }
       this.sliceFileAndUpload(fileHash, uploadedChunks); // 切片并上传
    },
    computeFileHash(file){
      return new Promise((resolve) => {
        const spark = new SparkMD5.ArrayBuffer();
        const fileReader = new FileReader();
        fileReader.onload = (e) => {
          spark.append(e.target.result);
          const hash = spark.end();
          resolve(hash); // 返回计算得到的hash值
        };
        fileReader.readAsArrayBuffer(file);
      });
    },
    checkFile(fileHash){
      //调用后端接口判断文件是否上传过
      //模拟接口 shouldUpload:是否应该上传,uploadedChunks:已经上传的切片
      return { shouldUpload: true, uploadedChunks: [] };
    },
    sliceFileAndUpload(fileHash, uploadedChunks){
      const chunkSize = 10 * 1024 * 1024; // 切片大小,这里是10MB
      this.chunkCount = Math.ceil(this.selectedFile.size / chunkSize); // 计算总切片数
      for (let i = 0; i < this.chunkCount; i++) {
        if (uploadedChunks.includes(i)) continue; // 跳过已上传的切片
        const chunk = this.selectedFile.slice(i * chunkSize, (i + 1) * chunkSize); // 获取切片
        this.requestPool.push({ chunk, index: i }); // 加入请求队列
      }
      this.processPool(fileHash); // 开始处理请求队列
    },
    processPool(fileHash){
      while (this.requestPool.length > 0 && this.MAX_REQUEST > 0) {
        const { chunk, index } = this.requestPool.shift(); // 取出一个待上传的切片
        this.uploadChunk(chunk, fileHash, index) // 上传切片
          .then(() => {
            this.uploadedChunksCount++; // 更新已上传切片数量
            if (this.requestPool.length > 0) {
              this.processPool(fileHash); // 继续处理请求队列
            } else if (this.uploadedChunksCount === this.chunkCount) {
              //TODO 所有切片都已上传,通知服务器合并
              this.mergeChunkFiles(fileHash);
            }
          })
          .finally(() => {
            this.MAX_REQUEST++; // 释放一个请求槽
          });
        this.MAX_REQUEST--; // 占用一个请求槽
      }
    },
    async uploadChunk(chunk, fileHash, index){
      const formData = new FormData();
      formData.append("chunk", chunk);
      formData.append("hash", fileHash);
      formData.append("index", index);
      // 替换为真实的上传URL
      // await axios.post("xxx", formData);
    },
    mergeChunkFiles(fileHash){
      // axios.post("xxx", fileHash);
    }
  }
}
</script>
​
​