Element的upload实现分片上传和断点续传功能

8,940 阅读2分钟

最近领导安排实习生做分片上传和断点续传的功能,实习生找我,之前刚好弄过了解一些就记录一下。我们需要上传一个大文件,比如上G的视频文件,通常我们后端会对上传文件进行限制,一般不宜过大,5MB左右最好。如果文件过大,超出了http服务端请求大小限制,请求时间超时,传输中断导致上传失败,那么我们可以将文件进行分片上传。

分片上传的原理

分片上传的原理就是在前端将文件分片,然后一片一片的传给服务端,由服务端对分片的文件进行合并,从而完成大文件的上传。分片上传可以解决文件上传过程中超时、传输中断造成的上传失败,而且一旦上传失败后,已经上传的分片不用再次上传,不用重新上传整个文件,因此采用分片上传可以实现断点续传以及跨浏览器上传文件。

使用Element的upload来实现

至于Element的引入就不多做赘述,直接贴代码。

<template>
  <div>
    <el-upload
      action
      :auto-upload="false"
      :show-file-list="false"
      :on-change="changeFile"
    >
      <el-button size="small" type="primary">选择文件</el-button>
      <div slot="tip" class="el-upload__tip">
        1.上传文件不超过100M<br />2.只能上传一个文件<br />3.等待进度条出现√才是上传完成
      </div>
    </el-upload>
    <!-- PROGRESS -->
    <div class="progress">
      <span>上传进度:{{ total | totalText }}%</span>
      <el-link
        type="primary"
        v-if="total > 0 && total < 100"
        @click="handleBtn"
        >{{ btn | btnText }}</el-link
      >
    </div>
  </div>
</template>

上面这部分是上传的页面代码部分,有上传、暂停、继续等基础功能。

<script>
import SparkMD5 from "spark-md5";
import { fileParse } from "@/public/utils.js";
import { getStorage } from "lesso-common/public/utils";
import axios from "axios";
export default {
  data() {
    return {
      total: 0,
      btn: false,
      abort: false,
      uploadSuc: false,
      slicesNum: null,
      chunkNumber: null,
      userInfo: {
        userName: getStorage("userData").user.employeeName,
        userId: getStorage("userData").user.userId,
        groupName: "AVM",
      },
    };
  },
  filters: {
    btnText(btn) {
      return btn ? "继续" : "暂停";
    },
    totalText(total) {
      return total > 100 ? 100 : total;
    },
  },
  methods: {
    // 分片上传
    async changeFile(file) {
      if (!file) return;
      file = file.raw;
      this.total = 0;
      this.abort = false;
      this.btn = false;

      let buffer = await fileParse(file, "buffer"),
        spark = new SparkMD5.ArrayBuffer(),
        hash,
        suffix;
      console.log(file, "file");
      spark.append(buffer);
      hash = spark.end();
      suffix = /\.([0-9a-zA-Z]+)$/i.exec(file.name)[1];

      let multiple = null;
      for (let i = 100; i > 1; i--) {
        if (file.size % i == 0 ) {
            multiple = i;
            break;
        }
      }

      // 切片个数
      this.slicesNum = multiple;

      // 创建切片
      let partList = [],
        partsize = file.size / this.slicesNum,
        cur = 0;

      for (let i = 1; i <= this.slicesNum; i++) {
        let item = {
          chunk: file.slice(cur, cur + partsize),
          filename: `${hash}_${i}.${suffix}`,
          chunkNumber: `${i}`,
          file: file.slice(cur, cur + partsize),
        };
        cur += partsize;
        partList.push(item);
      }
      this.requestData = {
        groupName: this.userInfo.groupName,
        fileMd5: hash,
        name: file.name,
        size: file.size,
        totalChunks: this.slicesNum,
        chunkSize: partsize,
        uid: this.userInfo.userId,
        uname: this.userInfo.userName,
      };
      this.partList = partList;
      this.sendRequest();
    },
    async sendRequest() {
      this.uploadSuc = false;
      // 根据100个切片创造100个请求集合
      let requestList = [];
      this.partList.forEach((item, index) => {
        // 每一个函数都发送一个切片请求
        let fn = async (chunkNumber) => {
          let formData = new FormData(),
            shardFile = new SparkMD5.ArrayBuffer(),
            shardFileBuffer = await fileParse(item.chunk, "buffer"),
            shardFileHash;
          shardFile.append(shardFileBuffer);
          shardFileHash = shardFile.end();
          formData.append(
            "chunkNumber",
            chunkNumber ? chunkNumber : item.chunkNumber
          );
          formData.append("groupName", this.requestData.groupName);
          formData.append("file", item.file);
          formData.append("fileMd5", this.requestData.fileMd5);
          formData.append("name", this.requestData.name);
          formData.append("size", this.requestData.size);
          formData.append("totalChunks", this.requestData.totalChunks);
          formData.append("chunkSize", this.requestData.chunkSize);
          formData.append("chunkMd5", shardFileHash);
          formData.append("uid", this.requestData.uid);
          formData.append("uname", this.requestData.uname);
          return axios
            .post(
              "http://iotupdateapi.lesso.com/uploadFastdfs/uploadPart",
              formData,
              {
                headers: { "Content-Type": "multipart/form-data" },
              }
            )
            .then((res) => {
              const { code, data } = res.data;
              if (code == 206) {
                this.total += parseInt(100 / this.slicesNum);
                // 传完的切片我们把它移除掉
                if (chunkNumber) {
                  this.partList.splice(data.chunkNumber - 1, 1);
                }

                return data.chunkNumber;
              } else if (code == 200) {
                this.uploadSuc = true;
              }
            });
        };
        requestList.push(fn);
      });

      let i = 0;

      let send = async () => {
        if (this.abort) return;
        if (i >= requestList.length && !this.uploadSuc) {
          this.$message.warning("上传失败,请重新上传");
          this.total = 0;
          return;
        }
        if (this.uploadSuc) {
          this.$message.success("上传成功");
          this.total = 100;
          return;
        }

        await requestList[i](this.chunkNumber).then((res) => {
          this.chunkNumber = res;
        });

        i++;
        send();
      };
      send();
    },
    handleBtn() {
      if (this.btn) {
        this.abort = false;
        this.btn = false;
        this.sendRequest();
        return;
      }

      this.btn = true;
      this.abort = true;
    },
  },
};
</script>

上面这部分是分片上传和断点续传的代码逻辑实现。
本来是想每次都直接创建100个切片进行上传,后来接口的参数要求每片切片的大小必须要是整数,所有就又做了一次处理,算出文件可以整除的数字,取最接近小于等于100的的数字进行切片处理。
把每个切片转换成每个请求放进数组,根据每次调接口的返回值来处理数组实现分片上传和断点续传。

utils的fileParse方法

export function fileParse(file, type = "base64") {
    return new Promise(resolve => {
        let fileRead = new FileReader();
        if (type === "base64") {
            fileRead.readAsDataURL(file);
        } else if (type === "buffer") {
            fileRead.readAsArrayBuffer(file);
        }
        fileRead.onload = (ev) => {
            resolve(ev.target.result);
        };
    });
};

再贴一下接口的参数

WechatIMG744.png

分片上传和断点续传demo

码云地址:gitee.com/wang_xiaohu…

如果有更好的方法,或者有错误帮忙指正。