断点续传作用:当上传文件时,大文件上传时耗时过长,如果遇到网络卡顿、断网等情况,再重新开始上传的体验感非常不好。前端优化分片上传文件,上传时把大文件分成很多个小文件,等到网络状况恢复了之后,即从之前上传断点处继续上传,提高用户体验。

实现思路:点击上传时获取上传文件,解析切片数据,设置切片大小,处理成多个切片数据,判断每个切片数据是否上传成功,未上传成功的部分继续上传,如果所有切片上传成功,合并全部已上传文件给后端接收。

vue+element ui 的上传 demo

<template>
  <div class="appUpload">
     <el-upload drag action
      :auto-upload="false" 
      :show-file-list="false" 
      :on-change="changeFile"
      :before-upload='beforeload'>
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">
        将文件拖到此处,或
        <em>点击上传</em>
      </div>
    </el-upload>
    
    <!-- PROGRESS -->
    <div class="progress">
      <el-progress :percentage="totalText"></el-progress>
      <el-link type="primary" v-if="totalText>0 && totalText<100" @click="handleBtn"> 
       {{btn===true?'暂停上传':'继续上传'}}</el-link>
    </div>
  </div>
</template>

js逻辑部分  

接口部分自行封装引入自己项目的接口  

//引入computeSlice函数(js封装的切片后每个切片的返回数据 传入原始文件和切片大小)
//引入beforeUpload函数上传之前校验
import { computeSlice ,beforeUpload} from '@/utils/fileutils'
beforeload(file) {
      beforeUpload(file)//上传前校验 根据自己的项目需求校验
    },


    //点击上传
    async changeFile(file) {
      this.uploadFile = file;
        let  size = 1024 * 1024 * 1;//设置切片大小
        let fileInfo =await computeSlice(file,size);//处理切片内容 获取切片fileInfo对象
        this.nSize = fileInfo.files.length;
        this.fileMd5 = fileInfo.fileMd5
        if( await this.getcheckFileMd5(fileInfo.info.suffix)){//如果该文件存在于数据库/之前上传过 则弹出提示 //终止以下程序
           return;
        }
            for(let i = 0; i < fileInfo.files.length; i++) {
                let status=await this.FileMd5(i,fileInfo.fileMd5)
            if(status && this.btn){
              await this.sendRequest(i,fileInfo.fileMd5,fileInfo.files[i])
            }
            }
        if(this.total  == this.nSize){
            this.getComplete(fileInfo.fileMd5);
        }
    },
//验证是否上传过该文件
    async getcheckFileMd5(suffix){
      let data={
        checkFileMd5:this.fileMd5,
        suffix:suffix
      }
      var  status;
      //checkFileMd5为封装后判断该视频是否上传接口 自行导入自己项目的后端接口即可
      await checkFileMd5(data).then((res) => {
                    if (res.data) {
                       this.$message({
                          message: '该视频已上传',
                        });
                    }
                    status= res.data;
                });
                return status;
    },
    //判断分片是否上传接口
     async FileMd5(num,value){
      let status;
       //checkChunk为封装后判断该视频每块分片是否上传接口 自行导入自己项目的后端接口即可
        await checkChunk({fileIndex:num,fileMd5:value}).then((res) => {
             if(res.data == false){
                status= true;
             }else{
              this.total++;
               status=  false;
             }
            });
            return status;
      },
     //上传接口
      async  sendRequest(num,value,item) {
        let chunkFile = new FormData();
                chunkFile.append("chunkFile", item);
       //chunkFileUpload为封装后上传接口 自行导入自己项目的后端接口即可
                await  chunkFileUpload(num,value,chunkFile).then((res) => {
                    if (res.code == 200) {
                        this.total ++;
                        this.totalText = Math.floor(this.total / this.nSize *100)
                    }
                });
    },
    //合并上传接口
    getComplete(fileMd5){
          let  params= {
              chunkTotal:this.nSize,
              fileMd5: fileMd5,
            }
            //mergeChunk为封装后合并上传接口 自行导入自己项目的后端接口即可
            mergeChunk(params).then((res) => {
                    if (res.code == 200) {
                        this.$message({
                          message: '上传成功',
                          type: 'success'
                        });
                        this.total = 0;
                        this.totalText = 100;
                    }
                });
    },

    //点击该按钮 暂停或继续上传分片 未上传或上传完毕后该按钮隐藏
    handleBtn() {
        if (this.btn) {
            // 断点续传
            this.btn = false;
            return;
        }else{
        this.btn = true;
        this.total = 0;
        this.totalText= 0;
        this.changeFile(this.uploadFile);
        }
    }

  utils目录下fileutils文件封装  需要用到spark-md5的MD5值 要npm引入

npm install --save spark-md5
let SparkMD5 = require('spark-md5')
// let SparkMD5 = require('spark-md5')
import SparkMD5 from 'spark-md5'//引入spark-md
//解析数据
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);
        };
    });
}

//计算切片方法 异步改成同步方法
export async function  computeSlice(file,chunkSize){
    if (!file) return;
    var fileInfo={
        fileMd5:'',
        files:[],
        info:{}
    };
    fileInfo.info.filename=file.name;
    file = file.raw;
     // 解析成buffer数据
    // 切片处理,把文件切成多个部分(固定数量/固定大小)
    // 每一个切片都有自己的部分数据和自己的名字
    let buffer ;
    await fileParse(file, "buffer").then((res)=>{
        buffer =res;
    });
    let  spark = new SparkMD5.ArrayBuffer();
        spark.append(buffer);//元素后面追加一个字符串
        fileInfo.fileMd5= spark.end();
        fileInfo.info.suffix = '.'+/\.([0-9a-zA-Z]+)$/i.exec(file.name)[1];

    let cur = 0;
    let  nSize =Math.ceil(file.size / chunkSize) 
        for(let i = 0; i < nSize; i++) {
            fileInfo.files.push(file.slice(cur, cur + chunkSize));
            cur += chunkSize;
        }
    return fileInfo;//输出一个切片后对象 fileInfo.files为切片后数组
}

//上传前校验  自行根据自己项目的需求进行校验
export  function beforeUpload(file) {
    // 格式校验
    let { type, size } = file;
     console.log(type)
    // if (!/(png|gif|jpeg|jpg)/i.test(type)) {
    //   this.$message("文件合适不正确~~");
    //   return false;
    // }
      if (type != "video/mp4") {
      this.$message("文件合适不正确~~");
      return false;
    }

    if (size > 200 * 1024 * 1024) {
      this.$message("文件过大,请上传小于200MB的文件~~");
      return false;
    }
    return true;
  }