vue+core 断点续传-上传-前后台实现

整体逻辑如下

       前台引用spark-md5获取文件唯一ID值,即md5值,前台将文件进行分片,通过该值进行后台校验,后台以MD5值生成文件夹,以断点生成子文件,以此实现断点续传。

       前台计算MD5,前台计算MD5快慢与文件大小相关,成功后方可请求后台,以防无值出现上传文件错乱。

      后台用MD5值进行校验,有则跳过,无则追加,文件分片完全上传后,再请求合并接口将文件进行合并。

       代码实现逻辑,前台将文件进行分片执行for循环,循环的每片值为该断点位置,将每一片进行记录请求到后台breakpoint参数,如果出现意外结束,再次上传时请求后台方法CheckPoint进行校验(校验逻辑,取出MD5文件夹下最大的文件名作为断点,存储时按断点传就直接取文件名,如果有内置逻辑,就按逻辑截取),如果存在则进行从该断点处上传,如果不存在从头开始,暂停按钮同理,所有分片数据请求成功后,请求FileMerge进行合并。

相应注释见代码,具体前后台代码如下:

前台:

<template>
  <div id="app">
    <!-- 上传组件 -->
    <el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">
        将文件拖到此处,或
        <em>点击上传</em>
      </div>
      <div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div>
    </el-upload>

    <!-- 进度显示 -->
    <div class="progress-box">
      <span>上传进度:{{ percent.toFixed() }}%</span>
      <el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button>
    </div>

    <!-- 展示上传成功的视频 -->
    <div v-if="videoUrl">
      <video :src="videoUrl" controls />
    </div>
  </div>
</template>
 
<script>
import SparkMD5 from "spark-md5";
import axios from "axios";

export default {
  name: "App3",
  filters: {
    btnTextFilter(val) {
      return val ? "暂停" : "继续";
    }
  },
  data() {
    return {
      percent: 0,
      videoUrl: "",
      upload: true,
      percentCount: 0,
      CurFileName: "",
      baseurllocal: window.localStorage.getItem("rooturl"),
      curPoint: 0,
      calchunk: 0
    };
  },
  methods: {
    ReadFileMd5(fileObj) {
      return new Promise((resolve, reject) => {
        let hash;
        const slicelength = 10;
        const rfmchunkSize = Math.ceil(fileObj.size / slicelength);
        const fileReader = new FileReader();
        const md5 = new SparkMD5();
        let index = 0;
        const loadFile = () => {
          const slice = fileObj.slice(index, index + rfmchunkSize);
          fileReader.readAsBinaryString(slice);
        };
        loadFile();
        fileReader.onload = e => {
          md5.appendBinary(e.target.result);
          if (index < fileObj.size) {
            index += rfmchunkSize;
            loadFile();
          } else {
            hash = md5.end();
            this.hash = hash;
            console.log(hash);
            resolve(hash);
          }
        };
      });
    },
    async handleChange(file) {
      if (!file) return;
      this.percentCount = 0;
      this.percent = 0;
      this.videoUrl = "";
      // 获取文件并转成 ArrayBuffer 对象
      const fileObj = file.raw;
      this.hash = "";
      this.ReadFileMd5(fileObj).then(response => {
        const hash = this.hash;
        // 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
        const chunkSize = 10485760, //2097152,
          chunkList = [], // 保存所有切片的数组
          chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
          suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1]; // 文件后缀名

        // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
        let curChunk = 0; // 切片时的初始位置
        for (let i = 0; i < chunkListLength; i++) {
          const item = {
            breakPoint: i.toString(),
            chunk: fileObj.slice(curChunk, curChunk + chunkSize),
            fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名
          };
          curChunk += chunkSize;
          chunkList.push(item);
        }
        console.log(chunkList);
        this.chunkList = chunkList;
        this.CurFileName = file.raw.name;
        console.log(this.CurFileName);
        console.log("chunkListLength" + "------" + chunkListLength);
        this.CheckPoint().then(res => {
          if (this.percentCount === 0) {
            this.percentCount = 100 / this.chunkList.length;
          }
          if (this.calchunk > 0) {
            this.percent += this.percentCount * this.calchunk; // 改变进度
            this.reqChuck(this.calchunk);
          } else {
            this.reqChuck(0);
          }
        });
      });
    },
    async reqChuck(curchunk) {
      let vm = this;
      await vm.reqChuckPoint(curchunk);
    },
    async reqChuckPoint(index) {
      this.curPoint = index;
      if (!this.upload) {
        return;
      } else {
        if (index > this.chunkList.length - 1) {
          this.complete();
          // this.percent = 100;
          console.log("循环结束");
          return;
        } else {
          const item = this.chunkList[index];
          const formData = new FormData();
          formData.append("md5", this.hash);
          formData.append("breakPoint", item.breakPoint);
          formData.append("chunk", item.chunk);
          formData.append("fileName", item.fileName);
          let url = this.baseurllocal + "/api/weatherforecast/UploadEfilePoint";
          console.log(url);
          await this.$api.post(url, formData, res => {
            if (res.code === 200) {
              // 成功
              if (this.percentCount === 0) {
                // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
                this.percentCount = 100 / this.chunkList.length;
              }
              this.percent += this.percentCount; // 改变进度
              console.log(this.percent);
              // this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
              index++;
              this.reqChuck(index);
            } else {
              this.upload = !this.upload;
              console.log("code不为200" + index);
            }
          });
        }
      }
    },
    complete() {
      if (!this.upload) return;
      console.log(this.baseurllocal + "/api/weatherforecast/FileMerge");
      this.$api.post(
        this.baseurllocal + "/api/weatherforecast/FileMerge",
        { hash: this.hash, filename: this.CurFileName },
        res => {
          if (res.code === 200) {
            // 请求发送成功
            console.log("合并成功");
            // this.videoUrl = res.data
          }
        }
      );
    },
    // 按下暂停按钮
    handleClickBtn() {
      this.upload = !this.upload;
      if (this.upload) {
        // 如果不暂停则继续上传
        if (this.percent >= 100) {
          this.percentCount = 0;
          this.percent = 0;
        }
        this.CheckPoint().then(res => {
          if (this.percentCount === 0) {
            this.percentCount = 100 / this.chunkList.length;
          }
          console.log(this.calchunk);
          console.log(this.curPoint);
          if (this.calchunk > 0 && this.calchunk >= this.curPoint - 1) {
            this.percent = this.percentCount * (this.curPoint - 1); // 改变进度
            this.reqChuck(this.curPoint - 1);
          } else {
            this.reqChuck(0);
          }
        });
      }
    },
    CheckPoint() {
      this.calchunk = 0;
      return new Promise((resolve, reject) => {
        this.$api.get(
          "api/weatherforecast/CheckPoint",
          { hash: this.hash },
          res => {
            if (res.code == 200) {
              this.calchunk = res.data;
              resolve(res.data);
            } else {
              this.calchunk = 0;
              resolve();
            }
          }
        );
      });
    }
  }
};
</script>
 
<style scoped>
.progress-box {
  box-sizing: border-box;
  width: 360px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
  padding: 8px 10px;
  background-color: #ecf5ff;
  font-size: 14px;
  border-radius: 4px;
}
</style>

后台net core代码如图

 

 

 

posted @ 2022-09-20 09:59  zwbsoft  阅读(184)  评论(0编辑  收藏  举报