vue使用fingerprintjs2优化分片上传断点续传,当同一个账号在不同电脑或者浏览器在文件上传时再继续上传改文件会失败的改动

罗烨霖
2023-12-01

前一篇文章说了分片上传和断点续传,这篇文章是来说说做了异步任务出现的bug,以及优化一下上传:

  1. 上一篇说了后端为了合并大文件不超时写了异步任务,前端去请求合并接口的时侯会给我先返回然后后端执行异步任务,而我只要开一个定时器每隔几秒去请求列表,直到列表里所有数据的状态都是合并成功清除定时器。(表格中是合并状态的数据不能进行操作)
  2. 然后我们发现一个bug:当一个账号在上传一个文件时,用同一个账号在另一个电脑或者浏览器也上传这个文件时,上传一切都是正常的,但是第一个上传成功后走到合并任务中过几秒合并成功,但是后面一个上传成功后一直在合并状态。这是因为后端在上传文件合并文件成功后会删了分片这也就是为什么后一个一直不能合并成功的原因。因为都是同一个文件,所以文件的md5值都是一样的,后端希望我传一个不同浏览器的唯一标识给他,也就是我将文件的md5值加上浏览器的唯一标识再进行md5,这样就可以保证即使一个文件在上传过程中继续在另一个地方再上传这个文件两个唯一标识是不一样的。(有想过不用fingerprintjs2用时间戳拼接加密,但是这样做不到断点续传了。。。)不管怎么说,砖还是要搬的,就用fingerprintjs2趴
  3. fingerprintjs2是浏览器的指纹标识,就是不同的浏览器这个标识是不一样的,比如同一个电脑,谷歌和360的就不一样;不同电脑的都是谷歌浏览器,fingerprintjs2的值也不一样
  4. 这个问题算是解决了,但我又发现一个新的bug:当我关闭上传弹框时我并不会去打断这个上传进度,也就是说一个文件上传过程中我关闭弹框再打开上传这个文件又是合并出现问题,那这个问题我是写一个全局变量flag,当我打开上传弹框时这个flag是true,关闭就给用户提示会打断上传进程然后令flag为false;
  5. 上代码吧:

组件部分

 <!-- 添加或修改视频对话框 -->
    <el-dialog
      :title="title"
      :visible.sync="open"
      width="700px"
      append-to-body
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      :before-close="handleDialogClose"
    >
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-row>
          <el-col :span="24">
            <el-form-item label="视频简介">
              <el-input
                v-model="form.description"
                type="textarea"
                placeholder="请输入视频简介"
                :autosize="{ minRows: 3 }"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="form.id === null">
          <el-col
            :span="24"
            style="display: flex; flex-direction: column; align-items: center"
          >
            <el-upload
              class="upload-demo"
              action="#"
              :http-request="requestUpload"
              :show-file-list="false"
              :before-upload="beforeUpload"
              drag
              :accept="'video/*'"
              style="display: flex; flex-direction: column; align-items: center"
              v-if="file === null"
            >
              <i class="el-icon-upload"></i>
              <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
              <div class="el-upload__tip" slot="tip">只能上传视频文件</div>
            </el-upload>
            <div v-else style="width: 100%; text-align: center">
              <p>{{ file.name }}</p>
              <el-progress :percentage="percentage" v-if="percentage !== 0"></el-progress>
            </div>
          </el-col>
        </el-row>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm" :loading="confirmLoading"
          >确 定</el-button
        >
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>

js部分

data() {
    return {
      disabled: false,
      file: null,
      percentage: 0,
      confirmLoading: false,
      //列表有状态不是1的定时器
      timer: null,
      //弹框没被关闭
      flag: true,
    };
  },
  created() {
    this.getList();
  },
  methods: {
    /** 查询视频列表 */
    getList() {
      clearInterval(this.timer);
      this.timer = null;
      // this.loading = true;
      listVideo(this.queryParams).then((response) => {
        this.videoList = response.rows;
        this.total = response.total;
        // this.loading = false;
        let needRefresh = this.videoList.every((r) => r.status === 1);
        if (needRefresh) {
          clearInterval(this.timer);
          this.timer = null;
        } else {
          this.timer = setInterval(() => {
            this.getList();
          }, 5 * 1000);
        }
      });
    },
    //监听弹框右上角关闭回调
    handleDialogClose() {
      //新增
      if (this.form.id == null) {
        this.$confirm("此操作将打断上传进程, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            this.flag = false;
            this.open = false;
            this.reset();
          })
          .catch(() => {});
      } else {
        this.open = false;
        this.reset();
      }
    },
    // 取消按钮
    cancel() {
      //新增
      if (this.form.id == null) {
        this.$confirm("此操作将打断上传进程, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            this.flag = false;
            this.open = false;
            this.reset();
          })
          .catch(() => {});
      } else {
        this.open = false;
        this.reset();
      }
    },
    // 表单重置
    reset() {
      this.form = {
      };
      this.resetForm("form");
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    /** 新增按钮操作 */
    handleAdd() {
      this.reset();
      this.disabled = false;
      this.file = null;
      this.open = true;
      this.confirmLoading = false;
      this.percentage = 0;
      this.flag = true;
      this.title = "添加视频";
    },
    /** 修改按钮操作 */
    handleUpdate(row) {
      if (row.status != 1) {
        this.$modal.msgError("当前文件正在合并或者合并错误,不能进行操作");
      } else {
        this.reset();
        this.disabled = true;
        const id = row.id || this.ids;
        getVideo(id).then((response) => {
          this.form = response.data;
          this.open = true;
          this.title = "修改视频";
        });
      }
    },
    // 覆盖默认的上传行为
    requestUpload() {},
    //上传前的操作
    beforeUpload(file) {
      if (
        file.type != "video/mp4"
      ) {
        this.$modal.msgError("视频上传只支持Mp4格式");
        return false;
      } else {
        this.file = file;
      }
    },
    //断点分片上传
    uploadByPieces({ file, pieceSize = 2, success, error }) {
      // 上传过程中用到的变量
      let fileMD5 = ""; // 总文件列表
      const chunkSize = pieceSize * 1024 * 1024; // 3MB一片
      const chunkCount = Math.ceil(file.size / chunkSize); // 总片数

      //得到某一片的分片
      const getChunkInfo = (file, currentChunk, chunkSize) => {
        let start = currentChunk * chunkSize;
        let end = Math.min(file.size, start + chunkSize);
        let chunk = file.slice(start, end);
        return chunk;
      };

      // 获取md5
      const readFileMD5 = () => {
        // 读取视频文件的md5
        console.log("获取文件的MD5值");
        Fingerprint2.get((components) => {
          const values = components.map((component, index) => {
            if (index === 0) {
              //把微信浏览器里UA的wifi或4G等网络替换成空,不然切换网络会ID不一样
              return component.value.replace(/\bNetType\/\w+\b/, "");
            }
            return component.value;
          });

          // 生成最终id murmur
          const murmur = Fingerprint2.x64hash128(values.join(""), 31);

          const startChunk = getChunkInfo(file, 0, chunkSize);
          let fileRederInstance = new FileReader();
          fileRederInstance.readAsBinaryString(startChunk);
          fileRederInstance.addEventListener("load", (e) => {
            let fileBolb = e.target.result;
            fileMD5 = md5(md5(fileBolb) + murmur);
            console.log(fileMD5);
            uploadCheckAxios({ identifier: fileMD5, totalChunks: chunkCount })
              .then((res) => {
                if (res.data.needMerge == true) {
                  console.log("文件都已上传,现在需要合并");
                  //调用合并接口
                  let time = new Date().getTime();
                  mergeFileAxios({
                    name: this.form.name,
                    subjectName: this.form.subjectName,
                    teacherName: this.form.teacherName,
                    categoryId: this.form.categoryId,
                    description: this.form.description,
                    identifier: fileMD5,
                    totalSize: file.size,
                    filename: time + "_" + file.name,
                  })
                    .then((res) => {
                      console.log("文件合并成功");
                      success && success(res);
                    })
                    .catch((e) => {
                      console.log(e, "文件合并错误");
                      error && error(e);
                    });
                } else {
                  //文件未被上传
                  if (res.data.status === 0) {
                    console.log("文件未被上传");
                    let needUploadList = [];
                    for (let i = 0; i < chunkCount; i++) {
                      needUploadList.push(i);
                    }
                    console.log(needUploadList);
                    readChunkMD5(needUploadList);
                  }
                  //文件已被上传过一部分
                  else {
                    let arr = res.data.uploaded;
                    let per = 100 / chunkCount;
                    this.percentage = Number((arr.length * per).toFixed(2));
                    console.log(this.percentage);
                    console.log(arr);
                    let needUploadList = [];
                    console.log("文件已被上传过一部分");
                    for (let i = 0; i < chunkCount; i++) {
                      if (!arr.includes(i)) {
                        needUploadList.push(i);
                      }
                    }
                    console.log(needUploadList);
                    readChunkMD5(needUploadList);
                  }
                }
              })
              .catch((e) => {
                error && error(e);
              });
          });
        });
      };

      // 针对每个文件进行chunk处理
      const readChunkMD5 = (needUploadList) => {
        if (this.flag) {
          if (needUploadList.length > 0) {
            let i = needUploadList[0];
            console.log(i);
            const chunk = getChunkInfo(file, i, chunkSize);
            let fetchForm = new FormData();
            fetchForm.append("chunkNumber", i);
            fetchForm.append("chunkSize", chunkSize);
            fetchForm.append("currentChunkSize", chunk.size);
            fetchForm.append("file", chunk);
            fetchForm.append("filename", fileMD5 + "-" + i);
            fetchForm.append("identifier", fileMD5);
            fetchForm.append("totalChunks", chunkCount);
            fetchForm.append("totalSize", file.size);
            uploadVideoChunkAxios(fetchForm)
              .then((res) => {
                console.log(res);
                //都上传了,等待合并
                if (res.data === true) {
                  console.log("文件开始合并");
                  let time = new Date().getTime();
                  mergeFileAxios({
                    name: this.form.name,
                    subjectName: this.form.subjectName,
                    teacherName: this.form.teacherName,
                    categoryId: this.form.categoryId,
                    description: this.form.description,
                    identifier: fileMD5,
                    totalSize: file.size,
                    filename: time + "_" + file.name,
                  })
                    .then((res) => {
                      console.log("文件合并成功");
                      success && success(res);
                    })
                    .catch((e) => {
                      console.log(e, "文件合并错误");
                      error && error(e);
                    });
                } else {
                  let per = 100 / chunkCount;
                  console.log(per, this.percentage);
                  let totalPrecent = Number((this.percentage + per).toFixed(2));
                  if (totalPrecent > 100) {
                    this.percentage === 100;
                  } else {
                    this.percentage = totalPrecent;
                  }
                  console.log(this.percentage);
                  let newArr = JSON.parse(JSON.stringify(needUploadList));
                  if (newArr.length > 0) {
                    newArr.shift();
                    readChunkMD5(newArr);
                  }
                }
              })
              .catch((e) => {
                error && error(e);
              });
          } else {
            console.log("上传结束");
          }
        } else {
          return;
        }
      };

      readFileMD5(); // 开始执行代码
    },
    /** 提交按钮 */
    submitForm() {
      this.$refs["form"].validate((valid) => {
        if (valid) {
          if (this.form.id != null) {
            updateVideo(this.form).then((response) => {
              this.$modal.msgSuccess("修改成功");
              this.open = false;
              this.getList();
            });
          } else {
            if (this.file != null) {
              this.confirmLoading = true;
              this.percentage = 0;
              this.uploadByPieces({
                file: this.file, //视频实体
                pieceSize: 3, //分片大小
                success: (res) => {
                  this.confirmLoading = false;
                  this.percentage = 100;
                  this.$modal.msgSuccess(res.msg);
                  this.open = false;
                  this.getList();
                },
                error: (e) => {
                  this.confirmLoading = false;
                  this.file = null;
                  this.percentage = 0;
                  this.$modal.msgError("上传视频失败,请重新上传");
                },
              });
            } else {
              this.$modal.msgError("请上传视频");
            }
          }
        }
      });
    },
    /** 删除按钮操作 */
    handleDelete(row) {
      if (row.status != 1) {
        this.$modal.msgError("当前文件正在合并或者合并错误,不能进行操作");
      } else {
        const ids = row.id || this.ids;
        this.$modal
          .confirm("是否确认删除选中视频的数据项?")
          .then(function () {
            return delVideo(ids);
          })
          .then(() => {
            this.$modal.msgSuccess("删除成功");
            this.getList();
          })
          .catch(() => {});
      }
    },
    /** 详细按钮操作 */
    handleView(row) {
      if (row.status != 1) {
        this.$modal.msgError("当前文件正在合并或者合并错误,不能进行操作");
      } else {
        this.$router.push(`/resource/video-detail/index/${row.id}`);
      }
    },
    //不符合条件的多选框禁用
    selectable(row, index) {
      let isChecked = true;
      if (row.status != 1) {
        // 判断里面是否存在某个参数
        isChecked = false;
      } else {
        isChecked = true;
      }
      return isChecked;
    },
  },
  destroyed() {
    clearInterval(this.timer);
    this.timer = null;
  },
 类似资料: