百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

springboot整合vue2实现大文件分片上传、秒传、断点续传

connygpt 2024-12-18 14:55 5 浏览

实现说明

本篇文章主要介绍vue端的整合和技术知识点的分析,springboot后台代码及整合请参考上一篇文章:springboot整合vue2-uploader文件分片上传、秒传、断点续传

项目开源地址:https://gitee.com/msxy/qingfeng-vue

其他产品开源地址:https://gitee.com/msxy

安装uploader和spark-md5的依赖

npm install --save vue-simple-uploader
npm install --save spark-md5

mainjs导入uploader

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

创建uploader组件

<template>
  <div>
    <uploader
      :autoStart="false"
      :options="options"
      :file-status-text="statusText"
      class="uploader-example"
      @file-complete="fileComplete"
      @complete="complete"
      @file-success="fileSuccess"
      @files-added="filesAdded"
    >
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <p>将文件拖放到此处以上传</p>
        <uploader-btn>选择文件</uploader-btn>
        <uploader-btn :attrs="attrs">选择图片</uploader-btn>
        <uploader-btn :directory="true">选择文件夹</uploader-btn>
      </uploader-drop>
      <!-- <uploader-list></uploader-list> -->
      <uploader-files> </uploader-files>
    </uploader>
    <br />
    <a-button @click="allStart()" :disabled="disabled">全部开始</a-button>
    <a-button @click="allStop()" style="margin-left: 4px">全部暂停</a-button>
    <a-button @click="allRemove()" style="margin-left: 4px">全部移除</a-button>
  </div>
</template>

<script>
import axios from "axios";
import SparkMD5 from "spark-md5";
import storage from "store";
import { ACCESS_TOKEN } from '@/store/mutation-types'
export default {
  data() {
    return {
      skip: false,
      options: {
        target: "//localhost:8888/upload/chunk",
        // 开启服务端分片校验功能
        testChunks: true,
        parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
          return parsedTimeRemaining
            .replace(/\syears?/, "年")
            .replace(/\days?/, "天")
            .replace(/\shours?/, "小时")
            .replace(/\sminutes?/, "分钟")
            .replace(/\sseconds?/, "秒");
        },
        // 服务器分片校验函数
        checkChunkUploadedByResponse: (chunk, message) => {
          const result = JSON.parse(message);
          if (result.data.skipUpload) {
            this.skip = true;
            return true;
          }
          return (result.data.uploaded || []).indexOf(chunk.offset + 1) >= 0;
        },
        headers: {
          // 在header中添加的验证,请根据实际业务来
          "Access-Token": storage.get(ACCESS_TOKEN),
        },
      },
      attrs: {
        accept: "image/*",
      },
      statusText: {
        success: "上传成功",
        error: "上传出错了",
        uploading: "上传中...",
        paused: "暂停中...",
        waiting: "等待中...",
        cmd5: "计算文件MD5中...",
      },
      fileList: [],
      disabled: true,
    };
  },
  watch: {
    fileList(o, n) {
      this.disabled = false;
    },
  },
  methods: {
    fileSuccess(rootFile, file, response, chunk) {
      // console.log(rootFile);
      // console.log(file);
      // console.log(message);
      // console.log(chunk);
      const result = JSON.parse(response);
      console.log(result.success, this.skip);

      if (result.success && !this.skip) {
        axios
          .post(
            "http://127.0.0.1:8888/upload/merge",
            {
              identifier: file.uniqueIdentifier,
              filename: file.name,
              totalChunks: chunk.offset,
            },
            {
              headers: { "Access-Token": storage.get(ACCESS_TOKEN) }
            }
          )
          .then((res) => {
            if (res.data.success) {
              console.log("上传成功");
            } else {
              console.log(res);
            }
          })
          .catch(function (error) {
            console.log(error);
          });
      } else {
        console.log("上传成功,不需要合并");
      }
      if (this.skip) {
        this.skip = false;
      }
    },
    fileComplete(rootFile) {
      // 一个根文件(文件夹)成功上传完成。
      // console.log("fileComplete", rootFile);
      // console.log("一个根文件(文件夹)成功上传完成。");
    },
    complete() {
      // 上传完毕。
      // console.log("complete");
    },
    filesAdded(file, fileList, event) {
      // console.log(file);
      file.forEach((e) => {
        this.fileList.push(e);
        this.computeMD5(e);
      });
    },
    computeMD5(file) {
      let fileReader = new FileReader();
      let time = new Date().getTime();
      let blobSlice =
        File.prototype.slice ||
        File.prototype.mozSlice ||
        File.prototype.webkitSlice;
      let currentChunk = 0;
      const chunkSize = 1024 * 1024;
      let chunks = Math.ceil(file.size / chunkSize);
      let spark = new SparkMD5.ArrayBuffer();
      // 文件状态设为"计算MD5"
      file.cmd5 = true; //文件状态为“计算md5...”
      file.pause();
      loadNext();
      fileReader.onload = (e) => {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
          // 实时展示MD5的计算进度
          console.log(
            `第${currentChunk}分片解析完成, 开始第${
              currentChunk + 1
            } / ${chunks}分片解析`
          );
        } else {
          let md5 = spark.end();
          console.log(
            `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
              file.size
            } 用时:${new Date().getTime() - time} ms`
          );
          spark.destroy(); //释放缓存
          file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
          file.cmd5 = false; //取消计算md5状态
          file.resume(); //开始上传
        }
      };
      fileReader.onerror = function () {
        this.error(`文件${file.name}读取出错,请检查该文件`);
        file.cancel();
      };
      function loadNext() {
        let start = currentChunk * chunkSize;
        let end =
          start + chunkSize >= file.size ? file.size : start + chunkSize;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }
    },
    allStart() {
      console.log(this.fileList);
      this.fileList.map((e) => {
        if (e.paused) {
          e.resume();
        }
      });
    },
    allStop() {
      console.log(this.fileList);
      this.fileList.map((e) => {
        if (!e.paused) {
          e.pause();
        }
      });
    },
    allRemove() {
      this.fileList.map((e) => {
        e.cancel();
      });
      this.fileList = [];
    },
  },
};
</script>

<style>
.uploader-example {
  width: 100%;
  padding: 15px;
  margin: 0px auto 0;
  font-size: 12px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
.uploader-example .uploader-btn {
  margin-right: 4px;
}
.uploader-example .uploader-list {
  max-height: 440px;
  overflow: auto;
  overflow-x: hidden;
  overflow-y: auto;
}
</style>

Uploader组件使用

<template>
  <div>
    <a-button @click="uploadFile"> 资源上传 </a-button>
    <a-drawer
      title="资源上传"
      placement="right"
      width="640"
      :closable="false"
      :visible="visible"
      @close="onClose"
    >
      <upload></upload>
    </a-drawer>
  </div>
</template>

<script>
import Upload from "@/components/Upload/Index";
export default {
  name: "WelcomePage",
  data() {
    return {
      visible: true,
    };
  },
  components: {
    Upload,
  },
  methods: {
    uploadFile() {
      this.visible = true;
    },
    onClose() {
      this.visible = false;
    }
  },
};
</script>

<style lang="less" scoped>
</style>

核心方法分析

<uploader
      :autoStart="false"
      :options="options"
      :file-status-text="statusText"
      class="uploader-example"
      @file-complete="fileComplete"
      @complete="complete"
      @file-success="fileSuccess"
      @files-added="filesAdded"
    >
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <p>将文件拖放到此处以上传</p>
        <uploader-btn>选择文件</uploader-btn>
        <uploader-btn :attrs="attrs">选择图片</uploader-btn>
        <uploader-btn :directory="true">选择文件夹</uploader-btn>
      </uploader-drop>
      <!-- <uploader-list></uploader-list> -->
      <uploader-files> </uploader-files>
    </uploader>
    <br />
    <a-button @click="allStart()" :disabled="disabled">全部开始</a-button>
    <a-button @click="allStop()" style="margin-left: 4px">全部暂停</a-button>
    <a-button @click="allRemove()" style="margin-left: 4px">全部移除</a-button>

options参数分析

参考 simple-uploader.js 配置。此外,你可以有如下配置项可选:

parseTimeRemaining(timeRemaining, parsedTimeRemaining) {Function}用于格式化你想要剩余时间,一般可以用来做多语言。参数:

    • timeRemaining{Number}, 剩余时间,秒为单位
    • parsedTimeRemaining{String}, 默认展示的剩余时间内容,你也可以这样做替换使用:
parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
  return parsedTimeRemaining
    .replace(/\syears?/, '年')
    .replace(/\days?/, '天')
    .replace(/\shours?/, '小时')
    .replace(/\sminutes?/, '分钟')
    .replace(/\sseconds?/, '秒')
}

categoryMap {Object}

文件类型 map,默认:

{
  image: ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp'],
  video: ['mp4', 'm3u8', 'rmvb', 'avi', 'swf', '3gp', 'mkv', 'flv'],
  audio: ['mp3', 'wav', 'wma', 'ogg', 'aac', 'flac'],
  document: ['doc', 'txt', 'docx', 'pages', 'epub', 'pdf', 'numbers', 'csv', 'xls', 'xlsx', 'keynote', 'ppt', 'pptx']
}

autoStart {Boolean}默认 true, 是否选择文件后自动开始上传。

fileStatusText {Object}默认:

{
  success: 'success',
  error: 'error',
  uploading: 'uploading',
  paused: 'paused',
  waiting: 'waiting'
}

用于转换文件上传状态文本映射对象。

0.6.0 版本之后,fileStatusText 可以设置为一个函数,参数为 (status, response = null), 第一个 status 为状态,第二个为响应内容,默认 null,示例:

fileStatusText(status, response) {
  const statusTextMap = {
    uploading: 'uploading',
    paused: 'paused',
    waiting: 'waiting'
  }
  if (status === 'success' || status === 'error') {
    // 只有status为success或者error的时候可以使用 response

    // eg:
    // return response data ?
    return response.data
  } else {
    return statusTextMap[status]
  }
}

fileComplete方法

fileComplete(rootFile) {
  // 一个根文件(文件夹)成功上传完成。
  // console.log("fileComplete", rootFile);
  // console.log("一个根文件(文件夹)成功上传完成。");
},

complete方法

 complete() {
      // 上传完毕。
      // console.log("complete");
    },

fileSuccess方法

文件上传成功,进行合并。

fileSuccess(rootFile, file, response, chunk) {
      // console.log(rootFile);
      // console.log(file);
      // console.log(message);
      // console.log(chunk);
      const result = JSON.parse(response);
      console.log(result.success, this.skip);

      if (result.success && !this.skip) {
        axios
          .post(
            "http://127.0.0.1:8888/upload/merge",
            {
              identifier: file.uniqueIdentifier,
              filename: file.name,
              totalChunks: chunk.offset,
            },
            {
              headers: { "Access-Token": storage.get(ACCESS_TOKEN) }
            }
          )
          .then((res) => {
            if (res.data.success) {
              console.log("上传成功");
            } else {
              console.log(res);
            }
          })
          .catch(function (error) {
            console.log(error);
          });
      } else {
        console.log("上传成功,不需要合并");
      }
      if (this.skip) {
        this.skip = false;
      }
    },

filesAdded方法

文件选择完成,进行文件分片处理。

filesAdded(file, fileList, event) {
      // console.log(file);
      file.forEach((e) => {
        this.fileList.push(e);
        this.computeMD5(e);
      });
    },
    computeMD5(file) {
      let fileReader = new FileReader();
      let time = new Date().getTime();
      let blobSlice =
        File.prototype.slice ||
        File.prototype.mozSlice ||
        File.prototype.webkitSlice;
      let currentChunk = 0;
      const chunkSize = 1024 * 1024;
      let chunks = Math.ceil(file.size / chunkSize);
      let spark = new SparkMD5.ArrayBuffer();
      // 文件状态设为"计算MD5"
      file.cmd5 = true; //文件状态为“计算md5...”
      file.pause();
      loadNext();
      fileReader.onload = (e) => {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
          // 实时展示MD5的计算进度
          console.log(
            `第${currentChunk}分片解析完成, 开始第${
              currentChunk + 1
            } / ${chunks}分片解析`
          );
        } else {
          let md5 = spark.end();
          console.log(
            `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
              file.size
            } 用时:${new Date().getTime() - time} ms`
          );
          spark.destroy(); //释放缓存
          file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
          file.cmd5 = false; //取消计算md5状态
          file.resume(); //开始上传
        }
      };
      fileReader.onerror = function () {
        this.error(`文件${file.name}读取出错,请检查该文件`);
        file.cancel();
      };
      function loadNext() {
        let start = currentChunk * chunkSize;
        let end =
          start + chunkSize >= file.size ? file.size : start + chunkSize;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }
    },

allStart全部开始

 allStart() {
  console.log(this.fileList);
  this.fileList.map((e) => {
    if (e.paused) {
      e.resume();
    }
  });
},

allStop全部停止

    allStop() {
      console.log(this.fileList);
      this.fileList.map((e) => {
        if (!e.paused) {
          e.pause();
        }
      });
    },

allRemove全部移除

    allRemove() {
      this.fileList.map((e) => {
        e.cancel();
      });
      this.fileList = [];
    },

文件分片

vue-simple-uploader自动将文件进行分片,在optionschunkSize中可以设置每个分片的大小。

如图:对于大文件来说,会发送多个请求,在设置testChunkstrue后(在插件中默认就是true),会发送与服务器进行分片校验的请求,下面的第一个get请求就是该请求;后面的每一个post请求都是上传分片的请求

看一下发送给服务端的参数,其中chunkNumber表示当前是第几个分片,totalChunks代表所有的分片数,这两个参数都是都是插件根据你设置的chunkSize来计算的。

需要注意的就是在最后文件上传成功的事件中,通过后台返回的字段,来判断是否要再给后台发送一个文件合并的请求。

MD5的计算过程

断点续传及秒传的基础是要计算文件的MD5,这是文件的唯一标识,然后服务器根据MD5进行判断,是进行秒传还是断点续传。

file-added事件之后,就计算MD5,我们最终的目的是将计算出来的MD5加到参数里传给后台,然后继续文件上传的操作,详细的思路步骤是:

  1. 把uploader组件的autoStart设为false,即选择文件后不会自动开始上传
  2. 先通过 file.pause()暂停文件,然后通过H5的FileReader接口读取文件
  3. 将异步读取文件的结果进行MD5,这里我用的加密工具是spark-md5,你可以通过npm install spark-md5 --save来安装,也可以使用其他MD5加密工具。
  4. file有个属性是uniqueIdentifier,代表文件唯一标示,我们把计算出来的MD5赋值给这个属性 file.uniqueIdentifier = md5,这就实现了我们最终的目的。
  5. 通过file.resume()开始/继续文件上传。
computeMD5(file) {
      let fileReader = new FileReader();
      let time = new Date().getTime();
      let blobSlice =
        File.prototype.slice ||
        File.prototype.mozSlice ||
        File.prototype.webkitSlice;
      let currentChunk = 0;
      const chunkSize = 1024 * 1024;
      let chunks = Math.ceil(file.size / chunkSize);
      let spark = new SparkMD5.ArrayBuffer();
      // 文件状态设为"计算MD5"
      file.cmd5 = true; //文件状态为“计算md5...”
      file.pause();
      loadNext();
      fileReader.onload = (e) => {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
          // 实时展示MD5的计算进度
          console.log(
            `第${currentChunk}分片解析完成, 开始第${
              currentChunk + 1
            } / ${chunks}分片解析`
          );
        } else {
          let md5 = spark.end();
          console.log(
            `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
              file.size
            } 用时:${new Date().getTime() - time} ms`
          );
          spark.destroy(); //释放缓存
          file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
          file.cmd5 = false; //取消计算md5状态
          file.resume(); //开始上传
        }
      };
      fileReader.onerror = function () {
        this.error(`文件${file.name}读取出错,请检查该文件`);
        file.cancel();
      };
      function loadNext() {
        let start = currentChunk * chunkSize;
        let end =
          start + chunkSize >= file.size ? file.size : start + chunkSize;
        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }
    },

给file的uniqueIdentifier 属性赋值后,请求中的identifier即是我们计算出来的MD5

秒传及断点续传

在计算完MD5后,我们就能谈断点续传及秒传的概念了。

服务器根据前端传过来的MD5去判断是否可以进行秒传或断点续传:

  • a. 服务器发现文件已经完全上传成功,则直接返回秒传的标识。
  • b. 服务器发现文件上传过分片信息,则返回这些分片信息,告诉前端继续上传,即断点续传

在每次上传过程的最开始,vue-simple-uploader会发送一个get请求,来问服务器我哪些分片已经上传过了,这个请求返回的结果也有几种可能:

  • a. 如果是秒传,在请求结果中会有相应的标识,比如我这里是skipUploadtrue,且返回了url,代表服务器告诉我们这个文件已经有了,我直接把url给你,你不用再传了,这就是秒传
  • b. 如果后台返回了分片信息,这是断点续传。如图,返回的数据中有个uploaded的字段,代表这些分片是已经上传过的了,插件会自动跳过这些分片的上传。

图b1:断点续传情况下后台返回值

前端做分片检验:checkChunkUploadedByResponse

插件自己是不会判断哪个需要跳过的,在代码中由options中的checkChunkUploadedByResponse控制,它会根据 XHR 响应内容检测每个块是否上传成功了,成功的分片直接跳过上传
你要在这个函数中进行处理,可以跳过的情况下返回true即可。

checkChunkUploadedByResponse: function (chunk, message) {
	 let objMessage = JSON.parse(message);
     if (objMessage.skipUpload) {
         return true;
     }

     return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},

注:skipUploaduploaded 是我和后台商议的字段,你要按照后台实际返回的字段名来。


优化MD5计算

原uploader中计算MD5的方式为对整个文件直接计算MD5,很吃内存,容易导致浏览器崩溃
我改成了通过分片读取文件的方式计算MD5,防止直接读取大文件时因内存占用过大导致的网页卡顿、崩溃。

自定义的状态

(之前我就封装了几种自定义状态,最近总有小伙伴问怎么没有“校验MD5”,“合并中”这些状态,我就把我的方法写出来了,方式很笨,但是能实现效果)

插件原本只支持了successerroruploadingpausedwaiting这几种状态,

由于业务需求,我额外增加了“校验MD5”“合并中”“转码中”“上传失败”这几种自定义的状态

由于前几种状态是插件已经封装好的,我不能改源码,只能用比较hack的方式:
当自定义状态开始时,要手动调一下
statusSet方法,生成一个p标签盖在原本的状态上面;当自定义状态结束时,还要手动调用statusRemove移除该标签。

this.statusSet(file.id, 'merging');
this.statusRemove(file.id);

相关推荐

3分钟让你的项目支持AI问答模块,完全开源!

hello,大家好,我是徐小夕。之前和大家分享了很多可视化,零代码和前端工程化的最佳实践,今天继续分享一下最近开源的Next-Admin的最新更新。最近对这个项目做了一些优化,并集成了大家比较关注...

干货|程序员的副业挂,12个平台分享

1、D2adminD2Admin是一个完全开源免费的企业中后台产品前端集成方案,使用最新的前端技术栈,小于60kb的本地首屏js加载,已经做好大部分项目前期准备工作,并且带有大量示例代码,助...

Github标星超200K,这10个可视化面板你知道几个

在Github上有很多开源免费的后台控制面板可以选择,但是哪些才是最好、最受欢迎的可视化控制面板呢?今天就和大家推荐Github上10个好看又流行的可视化面板:1.AdminLTEAdminLTE是...

开箱即用的炫酷中后台前端开源框架第二篇

#头条创作挑战赛#1、SoybeanAdmin(1)介绍:SoybeanAdmin是一个基于Vue3、Vite3、TypeScript、NaiveUI、Pinia和UnoCSS的清新优...

搭建React+AntDeign的开发环境和框架

搭建React+AntDeign的开发环境和框架随着前端技术的不断发展,React和AntDesign已经成为越来越多Web应用程序的首选开发框架。React是一个用于构建用户界面的JavaScrip...

基于.NET 5实现的开源通用权限管理平台

??大家好,我是为广大程序员兄弟操碎了心的小编,每天推荐一个小工具/源码,装满你的收藏夹,每天分享一个小技巧,让你轻松节省开发效率,实现不加班不熬夜不掉头发,是我的目标!??今天小编推荐一款基于.NE...

StreamPark - 大数据流计算引擎

使用Docker完成StreamPark的部署??1.基于h2和docker-compose进行StreamPark部署wgethttps://raw.githubusercontent.com/a...

教你使用UmiJS框架开发React

1、什么是Umi.js?umi,中文可发音为乌米,是一个可插拔的企业级react应用框架。你可以将它简单地理解为一个专注性能的类next.js前端框架,并通过约定、自动生成和解析代码等方式来辅助...

简单在线流程图工具在用例设计中的运用

敏捷模式下,测试团队的用例逐渐简化以适应快速的发版节奏,大家很早就开始运用思维导图工具比如xmind来编写测试方法、测试点。如今不少已经不少利用开源的思维导图组件(如百度脑图...)来构建测试测试...

【开源分享】神奇的大数据实时平台框架,让Flink&amp;Spark开发更简单

这是一个神奇的框架,让Flink|Spark开发更简单,一站式大数据实时平台!他就是StreamX!什么是StreamX大数据技术如今发展的如火如荼,已经呈现百花齐放欣欣向荣的景象,实时处理流域...

聊聊规则引擎的调研及实现全过程

摘要本期主要以规则引擎业务实现为例,陈述在陌生业务前如何进行业务深入、调研、技术选型、设计及实现全过程分析,如果你对规则引擎不感冒、也可以从中了解一些抽象实现过程。诉求从硬件采集到的数据提供的形式多种...

【开源推荐】Diboot 2.0.5 发布,自动化开发助理

一、前言Diboot2.0.5版本已于近日发布,在此次发布中,我们新增了file-starter组件,完善了iam-starter组件,对core核心进行了相关优化,让devtools也支持对IAM...

微软推出Copilot Actions,使用人工智能自动执行重复性任务

IT之家11月19日消息,微软在今天举办的Ignite大会上宣布了一系列新功能,旨在进一步提升Microsoft365Copilot的智能化水平。其中最引人注目的是Copilot...

Electron 使用Selenium和WebDriver

本节我们来学习如何在Electron下使用Selenium和WebDriver。SeleniumSelenium是ThoughtWorks提供的一个强大的基于浏览器的开源自动化测试工具...

Quick &#39;n Easy Web Builder 11.1.0设计和构建功能齐全的网页的工具

一个实用而有效的应用程序,能够让您轻松构建、创建和设计个人的HTML网站。Quick'nEasyWebBuilder是一款全面且轻巧的软件,为用户提供了一种简单的方式来创建、编辑...