断点续传,大文件上传

134 阅读4分钟

断点续传

思路

1.通过md5 来分别对每一个文件分片来进行编码

2.对整个文件进行md5的编码

3.通过并发请求上传分片

4.上传完成的时候通知后端进行分片的合并,合并成一个文件,返回文件地址

5.如果中间中断了请求,再次上传文件的时候重新对整个文件以及文件的分片进行编码,调用接口去后端查询已经上传的分片,在对未上传的分片进行 并发请求上传分片,结束的时候通知后端合并文件

注意事项:

1.在上传不同的文件的时候,当文件改变的时候,需要重置下存储文件信息的数组,避免上一个文件的信息影响

//文件改变的时候
const fileChange = async (e) => {
  const { files } = e.target;
  //重置下数据
  resetChunkInfor();
  await splitChunk(files[0], size);
  searchFile();
};

2.在向后端查询已经上传完成的数据,需要前端将整个文件的数据删除掉 已经上传的数据,需要注意的是:如果是通过index的顺序来进行数据的删除的话,删除的数组需要在最里层来进行遍历删除。否则会引起删除数据不准的问题。

const getFileInfor = (fileDate) => {
  getinfor(fileDate)
    .then((res) => {
      if (res.code == 200) {
        chunkInfor.finishFilds = res.finish;
        //更新需要上传的数据
        res.finish.map((demo) => {
          chunkInfor.filds.forEach((item, index) => {
            //代表已经上传这个分片了
            if (demo == item) {
              //删除该分片编码
              chunkInfor.filds.splice(index, 1);
              //删除分片数据
              chunkInfor.fileBuffer.splice(index, 1);
            }
          });
        });
        if (res.fileUrl) {
          fileUrl.value = res.fileUrl;
          showPro.value = false;
        } else {
          showPro.value = true;
          fileUrl.value = "";
        }
      }
    })
    .catch((error) => {
      console.log(error);
    });
};

前端代码:

<template>
  <input type="file" @change="fileChange" />
  <el-button type="primary" @click="upChunks">上传</el-button>
  <el-progress v-if="showPro" :percentage="percentage" />
  <div>上传完成的文件地址:{{ fileUrl }}</div>
  <!-- <el-button @click="searchFile">查询数据</el-button> -->
  <!-- <el-button @click="conative">合并文件</el-button> -->
</template>
<script lan="js" setup>
import SparkMD5 from "spark-md5";
// import buffer from "buffer";
import { demoApi, getinfor, concatInfor } from "@/api/demo.js";
​
//MIME类型映射
import { mimeName, mimeType } from "../untils/MimeType";
import { computed, reactive, ref } from "vue";
// import { isArray } from "element-plus/es/utils";
const chunkInfor = reactive({
  fileMd: "", //存储文件的md5
  filds: [], //存储每一个分片的编码
  allFilds: [], //需要上传的全部分片编码
  fileBuffer: [], // 存储文件的buffer 数据
  fileType: "", // 文件的类型
  finishFilds: [] // 存储上传完成的分片数据
});
const fileUrl = ref("");
const showPro = ref(true);
const percentage = computed(() => {
  return Math.floor((chunkInfor.finishFilds.length / chunkInfor.allFilds.length) * 100 || 0);
});
​
const fileChange = async (e) => {
  const { files } = e.target;
  //重置下数据
  resetChunkInfor();
  await splitChunk(files[0], size);
  searchFile();
};
//重置下数据
const resetChunkInfor = () => {
  Object.assign(chunkInfor, {
    fileMd: "",
    filds: [],
    allFilds: [],
    fileBuffer: [],
    fileType: "",
    finishFilds: []
  });
};
//查询数据
const searchFile = () => {
  let { fileMd, filds, fileType } = chunkInfor;
  getFileInfor({
    fileMd,
    filds,
    fileType
  });
};
// 查询后端存储的文件信息
const getFileInfor = (fileDate) => {
  getinfor(fileDate)
    .then((res) => {
      if (res.code == 200) {
        chunkInfor.finishFilds = res.finish;
        //更新需要上传的数据
        res.finish.map((demo) => {
          chunkInfor.filds.forEach((item, index) => {
            //代表已经上传这个分片了
            if (demo == item) {
              //删除该分片编码
              chunkInfor.filds.splice(index, 1);
              //删除分片数据
              chunkInfor.fileBuffer.splice(index, 1);
            }
          });
        });
        if (res.fileUrl) {
          fileUrl.value = res.fileUrl;
          showPro.value = false;
        } else {
          showPro.value = true;
          fileUrl.value = "";
        }
      }
    })
    .catch((error) => {
      console.log(error);
    });
};
//合并文件
const conative = () => {
  let { fileMd, filds, fileType } = chunkInfor;
  concatfileInfor({
    fileMd,
    filds,
    fileType
  });
};
//合并文件
const concatfileInfor = (fileDate) => {
  // fileDate
  concatInfor(fileDate)
    .then((res) => {
      if (res.code == 200) {
        fileUrl.value = res.url;
      }
    })
    .catch((error) => {
      console.log(error);
    });
};
​
//10kB 进行切片
const size = 10 * 1024;
// 生成文件的切片
const splitChunk = (file, size = 10 * 1024) => {
  return new Promise(async (resolve, reject) => {
    //文件以字节为单位进行分割的
    // 用来存储 buffer 的数组
    const spark = new SparkMD5.ArrayBuffer();
    //分片的数量
    const chunkNumber = Math.ceil(file.size / size);
    //存储上传文件的类型
    if (chunkInfor.fileType == "") {
      mimeType.forEach((item, index) => {
        if (file.type == item) {
          chunkInfor.fileType = mimeName[index];
        }
      });
    }
    //当前分片的下标
    let chunkIndex = 0;
    // chunkDemo();
    // 生成文件读取器
    const fileReader = new FileReader();
    //文件读取成功
    fileReader.onload = async function (e) {
      // 将文件存储在buffer数组中
      spark.append(e.target.result);
​
      //存储文件的buffer数据
      chunkInfor.fileBuffer.push(new Blob([e.target.result]));
      // chunkInfor.fileBuffer.push(buffer.Buffer.from(e.target.result));
​
      //对当前分片进行md5 的编码
      const cunkMd5 = await SparkMD5.ArrayBuffer.hash(e.target.result);
      //放入每一个分片md5
      chunkInfor.filds.push(cunkMd5 + chunkIndex);
      chunkInfor.allFilds.push(cunkMd5 + chunkIndex);
      chunkIndex++;
      if (chunkIndex < chunkNumber) {
        //读取下一片分片
        readNext();
      } else {
        //对整个文件进行md5 加密处理
        chunkInfor.fileMd = spark.end();
        resolve(chunkInfor);
      }
    };
    //读取下一个分片
    async function readNext() {
      const start = chunkIndex * size;
      const end =start + size >= file.size ? file.size : start + size; 
      // 使用文件读取器读取该文件
      fileReader.readAsArrayBuffer(file.slice(start, end));
    }
    readNext();
  });
};
//上传切片
const upChunks = async () => {
  const finishresult = await concurRequest(chunkInfor.filds.length, 3);
};
​
//并发请求的封装
const concurRequest = async (allRequestNumber, maxnumber = 3) => {
  /**
   * 1.循环maxnumber  进行请求的发送
   * 2.在发送请求中 存储下 index, 用来回填请求成功之后的
   */
  let response = []; //用来存储每次请求的返回的结果
  let count = 0; // 用来存储接口请求成功或者失败的数量
  let index = 0; //下一次请求的index
  return new Promise((resolve, reject) => {
    function sendRequest() {
      const data = {
        fileMd: chunkInfor.fileMd,
        filds: chunkInfor.filds[index],
        fileBuffer: chunkInfor.fileBuffer[index]
      };
      const formData = new FormData();
      formData.append("fileMd", data.fileMd);
      formData.append("filds", data.filds);
      formData.append("file", data.fileBuffer);
      // 存储本次请求的index
      let seleIndex = index;
      index++;
      demoApi(formData)
        .then((res) => {
          count++;
          if (count > allRequestNumber - 1) {
            resolve(response);
            //合并文件接口
            conative();
          }
          if (index <= allRequestNumber - 1) {
            //递归调用
            sendRequest();
          }
          response[seleIndex] = "请求成功" + seleIndex;
          //存储上传成功的分片 计算上传进度条
          chunkInfor.finishFilds.push(data.filds);
        })
        .catch((error) => {
          count++;
          if (count > allRequestNumber - 1) {
            resolve(response);
            //合并文件接口
            conative();
          }
          if (index <= allRequestNumber - 1) {
            //递归调用
            sendRequest();
          }
          response[seleIndex] = "请求失败" + seleIndex;
        })
    }
    for (let i = 0; i < Math.min(allRequestNumber, maxnumber); i++) {
      sendRequest();
    }
  });
};
</script>
​
​