大文件切片上传

需求:对于比较大的文件一次性上传对服务器压力比较大,所以需要切片上传

思路:可以固定切片大小(每个小文件的大小)然后分段上传,也可以固定切片数量(然后计算每个切片大小进行上传)

<template>
  <el-upload
    action
    drag
    :auto-upload="false"
    :show-file-list="false"
    :on-change="handleChange"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <i class="el-icon-upload"></i>
    <div class="el-upload__text">
      将文件拖到此处,或
      <em>点击上传</em>
    </div>
    <div class="el-upload__tip" slot="tip">
      支持扩展名:pdf.doc.pptx.mp4.mp3,视频大小5G以内
      <div class="file_info" v-if="percent">
        <div class="ellipsis-one">{{ filename }}</div>
        <el-progress :percentage="Math.ceil(percent)"></el-progress>
      </div>
    </div>
  </el-upload>
</template>

<script>
import axios from "axios";
import SparkMD5 from "spark-md5";

export default {
  inheritAttrs: false, // 不继承父组件非 props 属性,如 class style
  data() {
    return {
      action: `${process.env.VUE_APP_API_PATH}/api/services/app/FileUpLoadService/FileUploadSingle`,
      percent: 0, // 总进度
      percentCount: 0, // 进度值
      isSuspend: true, // 是否暂停
      filename: "", // 文件名
    };
  },
  methods: {
    // * 处理文件变化
    async handleChange(file) {
      if (!file) return;
      this.$message.info("正在分析文件中...");
      this.filename = file.name;
      this.percent = 0;
      this.percentCount = 0;
      // 获取文件并转成 ArrayBuffer 对象
      const fileObj = file.raw;
      let buffer;
      try {
        buffer = await this.fileToBuffer(fileObj);
      } catch (error) {
        console.log("🚀 ~ handleChange ~ error:", error);
      }
      // 将文件按固定大小(5M)进行切片,注意此处同时声明了多个常量
      const chunkSize = 5 * 1024 * 1024,
        chunkList = [], // 保存所有切片的数组
        chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
        suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1]; // 文件后缀名

      // 根据文件内容生成 hash 值
      const spark = new SparkMD5.ArrayBuffer();
      spark.append(buffer);
      const hash = spark.end();

      // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
      let startPos = 0; // 切片时的初始位置
      for (let i = 0; i < chunkListLength; i++) {
        const item = {
          chunk: fileObj.slice(startPos, startPos + chunkSize),
          fileName: `${hash}_${i}.${suffix}`, // 文件名规则按照 hash_1.jpg 命名
        };
        startPos += chunkSize;
        chunkList.push(item);
      }
      this.chunkList = chunkList; // sendRequest 要用到
      this.hash = hash; // sendRequest 要用到
      this.sendRequest();
    },
    // * 发送切片
    async sendChunks(formData, index) {
      await axios({
        url: "http://127.0.0.1:3000/upload/bigFiles",
        method: "post",
        headers: { "Content-Type": "multipart/form-data", token: this.headers.token },
        data: formData,
      });
      // 成功
      if (this.percentCount === 0) {
        // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
        this.percentCount = 100 / this.chunkList.length;
      }
      this.percent += this.percentCount; // 改变进度
      this.chunkList.splice(index, 1); // 一旦上传成功就删除这一个 chunk,方便断点续传
    },
    // * 发送请求
    sendRequest() {
      const requestList = []; // 请求集合
      this.chunkList.forEach((item, index) => {
        const fn = () => {
          const formData = new FormData();
          formData.set("name", item.fileName);
          formData.append("chunks", item.chunk);
          return this.sendChunks(formData, index);
        };
        requestList.push(fn);
      });
      let i = 0; // 记录发送的请求个数
      // 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
      const complete = async () => {
        const result = await axios({
          url: "http://127.0.0.1:3000/upload/merge",
          method: "get",
          headers: { token: this.headers.token },
          params: { hash: this.hash },
        });
        this.$message.success("上传成功");
        console.log("上传成功--临时访问路径", result.data.data.url);
      };
      const send = async () => {
        if (!this.isSuspend) return;
        if (i >= requestList.length) {
          return complete(); // 发送完毕
        }
        await requestList[i]();
        i++;
        send();
      };
      send(); // 发送请求
    },
    // * 按下暂停按钮
    handleClickBtn() {
      this.isSuspend = !this.isSuspend;
      // 如果不暂停则继续上传
      if (this.isSuspend) this.sendRequest();
    },
    // * 将 File 对象转为 ArrayBuffer
    fileToBuffer(file) {
      return new Promise((resolve, reject) => {
        const fr = new FileReader();
        fr.onload = event => {
          resolve(event.target.result);
        };
        fr.readAsArrayBuffer(file);
        fr.onerror = event => {
          reject(new Error("转换文件格式发生错误-->" + event.target.error));
        };
      });
    },
  },
  computed: {
    headers() {
      return {
        Authorization: `Bearer ${this.$store.state.user.session.accessToken}`,
        // 自己的token 测试大文件上传
        token:
          "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuaWNrbmFtZSI6InBhbmdodSIsInBhc3N3b3JkIjoiMTIzNDU2IiwiaWF0IjoxNzA1NzU3Njk3LCJleHAiOjE3MDU4MDA4OTd9.JdNbM7Hed31opf2Ktagudy1FkGRh7jTIcIYedXJ_43U",
      };
    },
  },
};
</script>

<style lang="scss" scoped>
@import "@/styles/index.scss";
.file_info {
  margin-top: 7px;
  max-width: 360px;
}
.ellipsis-one {
  @include ellipsis-one;
}
</style>

这里使用的是固定切片大小(5Mb),计算出切片数量进行上传

  • 8
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值