vue3 + elementui实现前端大文件断点续传

619 阅读1分钟

原创:前端利用切片实现大文件断点续传 - 掘金 (juejin.cn))

我这里仔细阅读了原创的代码实现方式,对代码做出了改进,通过vue框架改了一下

完整代码gitee地址: gitee.com/z2695836546…

一.上传文件组件:

  • el-upload组件需要添加:auto-upload="false" ,禁止选择文件后自动上传
<template>
  <div class="box">
    <el-upload
      v-model:file-list="fileList"
      class="upload-demo"
      action=""
      multiple
      :on-change="change"
      :auto-upload="false"
      :show-file-list="false"
    >
      <el-button type="primary" class="upload_btn" :loading="uploadBtnLoading"
        >点击上传</el-button
      >
    </el-upload>
    <el-progress
      :percentage="percentage"
      :indeterminate="true"
      v-if="isShowProgress"
    />
  </div>
</template>

<script setup>
import { ref } from "vue";
import instance from "../utils/request.js";
import SparkMD5 from "spark-md5";
const isShowProgress = ref(false); //是否显示进度条
const uploadBtnLoading = ref(false); //上传按钮是否处于加载状态
const percentage = ref(0); //进度条数值
/**
 * 传入文件对象,返回文件生成的HASH值,后缀,buffer,以HASH值为名的新文件名
 * @param file
 * @returns {Promise<unknown>}
 */
const changeBuffer = (file) => {
  return new Promise((resolve) => {
    let fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onload = (ev) => {
      let buffer = ev.target.result,
        spark = new SparkMD5.ArrayBuffer(),
        HASH,
        suffix;
      spark.append(buffer);
      HASH = spark.end();
      suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
      resolve({
        buffer,
        HASH,
        suffix,
        filename: `${HASH}.${suffix}`,
      });
    };
  });
};

//选择文件后调用
const change = async (uploadFile) => {
  //文件状态处于待上传状态
  if (uploadFile.status === "ready") {
    let file = uploadFile.raw;
    if (!file) return;
    uploadBtnLoading.value = true;
    isShowProgress.value = true;
    // 获取文件的HASH
    let already = [], //已经上传过的切片的切片名
      data = null,
      { HASH, suffix } = await changeBuffer(file); //得到原始文件的hash和后缀

    // 获取已经上传的切片信息
    try {
      data = await instance.get("/upload_already", {
        params: {
          HASH,
        },
      });
      if (+data.code === 0) {
        already = data.fileList;
      }
    } catch (err) {}

    // 实现文件切片处理 「固定数量 & 固定大小」
    let max = 1024 * 100, //切片大小先设置100KB
      count = Math.ceil(file.size / max), //得到应该上传的切片
      index = 0, //存放切片数组的时候遍历使用
      chunks = []; //用以存放切片值
    if (count > 100) {
      //如果切片数量超过100,那么就只切成100个,因为切片太多的话也会影响调用接口的速度
      max = file.size / 100;
      count = 100;
    }
    while (index < count) {
      //循环生成切片
      //index 0 =>  0~max
      //index 1 =>  max~max*2
      //index*max ~(index+1)*max
      chunks.push({
        file: file.slice(index * max, (index + 1) * max),
        filename: `${HASH}_${index + 1}.${suffix}`,
      });
      index++;
    }
    index = 0;

    //每一次上传一个切片成功的处理[进度管控&切片合并]
    const complate = async () => {
      // 管控进度条:每上传完一个切片,就将进度条长度增加一点
      index++;
      percentage.value = ((index / count) * 100).toFixed(1);

      if (index < count) return;
      // 当所有切片都上传成功,就合并切片
      percentage.value = 100;
      try {
        //调用合并切片方法
        data = await instance.post(
          "/upload_merge",
          {
            HASH,
            count,
          },
          {
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
          }
        );
        if (+data.code === 0) {
          alert(
            `恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`
          );
          clear();
          return;
        }
        throw data.codeText;
      } catch (err) {
        alert("切片合并失败,请您稍后再试~~");
        clear();
      }
    };

    // 循环上传每一个切片
    chunks.forEach((chunk) => {
      // 已经上传的无需在上传
      //后台返回的already格式为['HASH_1.png','HASH_2.png'],既已经上传的文件的切片名
      if (already.length > 0 && already.includes(chunk.filename)) {
        //已经上传过了的切片就无需再调用接口上传了
        complate(); //动进度条或合并所有切片
        return;
      }
      let fm = new FormData();
      fm.append("file", chunk.file);
      fm.append("filename", chunk.filename);
      instance
        .post("/upload_chunk", fm)
        .then((data) => {
          //使用form data格式上传切片
          if (+data.code === 0) {
            complate(); ////动进度条或合并所有切片
            return;
          }
          return Promise.reject(data.codeText);
        })
        .catch(() => {
          alert("当前切片上传失败,请您稍后再试~~");
          clear();
        });
    });
  }
};
const clear = () => {
  //上传完成后,将状态回归
  isShowProgress.value = false;
  uploadBtnLoading.value = false;
  percentage.value = 0;
};
</script>

<style scoped>
.box {
  width: 20vw;
  margin: 20% auto;
}
.upload_btn {
  margin-bottom: 50px;
}
</style>

二,request.js组件(对axios做出的简单封装)

/*把axios发送请求的公共信息进行提取*/
//创建一个单独的实例,不去项目全局的或者其他的axios冲突
import axios from "axios";
import Qs from "qs";
let instance = axios.create();
instance.defaults.baseURL = "http://127.0.0.1:8888";
//默认是multipart/form-data格式
instance.defaults.headers["Content-Type"] = "multipart/form-data";
instance.defaults.transformRequest = (data, headers) => {
  //兼容x-www-form-urlencoded格式的请求发送
  const contentType = headers["Content-Type"];
  if (contentType === "application/x-www-form-urlencoded")
    return Qs.stringify(data);
  return data;
};
//统一结果的处理
instance.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (reason) => {
    //统一失败的处理
    return Promise.reject(reason);
  }
);
export default instance;

image.png