vue---大文件分片上传

65 篇文章 2 订阅

目录

1.获取切片信息

2.切片处理

3.切片分组

4.分片上传

5.进度条处理

6.完整代码


思路:将大文件切成小片异步上传可以解决大文件上传过慢的问题,但如果文件很大时,采用一次性异步上传,会在同一时间产生过多的请求,后端服务器处理不过来,导致请求异常。因此将文件切成小片后,再分组上传,比如一组10个切片,等待10个切片的请求上传完成后,再接着下一组,以此减轻对服务器的压力。

具体步骤如下:

1.获取切片信息

根据切片的大小对文件进行切片,可以前端自己规定,也可以后端返回,具体看业务要求。在以下案例中是后台确定分片大小,因此上传文件前先将文件信息(文件hash值,文件名,文件大小)传给后台,后台返回文件的切片大小,切片数量等信息

let params = {
  fileHash: Base64.stringify(Sha256(File.raw)),
  fileName: File.name,
  totalSize: File.size
}
const { data = {} } = await uploadInit(params) //初始化从后台获取分片数量

2.切片处理

前端根据切片信息进行切片处理,并将切片信息存放在formData对象中,这个对象就是要发给后台的数据了。

【let chunkList = this.sliceFile(File, data)】,File为文件,data为后台返回的数据,包括文件切片数量,切片大小

sliceFile(File, data) {
    let chunkList = []
    const { objectName, partCount, partSize } = { ...data }
    for (let i = 0; i < partCount; i++) {
      let start = i * partSize //分片开始位置
      let end = Math.min(File.size, start + partSize) //分片结束位置
      console.log("------分片",`${start}-${end}`);
      let _chunkFile = File.raw.slice(start, end)
      console.log(`第${i}片文件`, _chunkFile)
      let formData = new FormData()
      formData.append('index', i + 1) //后台分片索引从1开始
      formData.append('multipartFile', _chunkFile)
      formData.append('objectName', objectName)
      chunkList.push(formData)
    }
    return chunkList
  }

3.切片分组

切片后再把所有的切片分组【let groupList = this.groupChunkList(chunkList)】

  groupChunkList(chunkList) {
    let group = Math.ceil(chunkList.length / this.groupCount) //分组
    let resultList = new Array(group) // 根据组数创建一个空的 list
    chunkList.map((item, index) => {
      let i = Math.floor(index / this.groupCount) // index / count 向下取整 为组数编号
      if (Array.isArray(resultList[i])) resultList[i].push(item)
      else resultList[i] = [item]
    })
    return resultList
  }

4.分片上传

按组发送请求,上一组上传请求完成了再继续发送下一组请求,把上传失败的分片存储起来,再次进行上传

async uploadPartFile(list) {
    for (let i = 0; i < list.length; i++) {
      let itemList = Array.isArray(list[i]) ? list[i] : []
      let itemHttpUpload = itemList.map((item, index) => {
        return new Promise(async (resolve, reject) => {
          uploadPart(item)
            .then((res) => {
              if (res.code !== 200) {
                console.log(`第${item.get('index')}片上传失败,请重传`, res)
                reject(item)
              } else {
                  this.uploadPartCount++
                console.log(this.uploadPartCount,`第${item.get('index')}片上传成功啦!`, res)
                resolve(item)
              }
            })
            .catch((err) => {
              console.log('【文件异步上传报错】', err)
            })
        })
      })
      // Promise.allSettled  等待上一组 upload http请求所有都完成后 返回结果
      const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
        .then((res) => {
          res.forEach((item) => {
            console.log(item.status)
          })
          return Promise.resolve(res)
        })
        .catch((err) => {
          return Promise.resolve(err)
        })
      // 将上传失败的分片文件存储在errorChunkList
      let errorChunkList = lastGroupUpload
        .filter((item) => item.status === 'rejected')
        .map((item) => item.reason)
      // 上传失败的分片再次上传
      if (errorChunkList.length > 0) {
        errorChunkList.forEach((item) => {
          this.errorChunkUpload(item)
        })
      }
    }
    return true
  },    
  
  async errorChunkUpload(chunk) {
    let res = await uploadPart(chunk)
    if (res.code && res.code !== 200) {
      console.log(`上传失败,请重传`, res)
      this.errorChunkUpload(chunk) //失败则递归不断上传该片
    } else {
      console.log(`上传成功啦!`, res)
    }
  },

Promise.allSettled() 方法可以用于并行处理多个Promise,等待所有的Promise都执行完成后再做进一步处理。

5.进度条处理

//uploadPartCount:上传成功的切片数量;partCount: 文件切片数量
computed: {
    percent() {
      let { uploadPartCount, partCount } = this
      return Math.floor((uploadPartCount*100) / partCount) 
    }
  },
  watch:{
    percent:function(newValue,oldValue){
      console.log("----------watch-----",newValue,oldValue);
      if(newValue==100){
        setTimeout(()=>{
           this.uploadPartCount=0
        this.uploadFlag=false
        this.$refs.uploadFile.clearFiles()
        },1000) //有的文件小,进度条太快了,看不到效果,因此加了个定时
      }
    }
  },

6.完整代码

<template>
  <div class="myDiv">
    <el-upload
      class="upload-demo"
      action="#"
      :on-change="changeUpload"
      :show-file-list="false"
      :auto-upload="false"
      ref="uploadFile"
      :limit="1"
    >
      <el-button size="small" type="primary" :loading="loadingFile">
        上传文件
      </el-button>
    </el-upload>

    <el-progress
      v-show="uploadFlag"
      :text-inside="true"
      text-color="#fff"
      :stroke-width="18"
      :percentage="percent"
    ></el-progress>
  </div>
</template>

<script>
import { uploadInit, uploadPart } from '../../api/api'  //接口
import Sha256 from 'crypto-js/sha256'
import Base64 from 'crypto-js/enc-base64'
export default {
  data() {
    return {
      loadingFile: false,//上传按钮的loading
      partCount: 1, //文件切片数量
      groupCount: 5, //一次性发送切片请求的个数 默认 5
      uploadFlag: false,//正在上传,用于展示进度条
      uploadPartCount: 0 //上传成功的切片数量
    }
  },
  computed: {
    percent() {
      let { uploadPartCount, partCount } = this
      return Math.floor((uploadPartCount*100) / partCount) 
    }
  },
  watch:{
    percent:function(newValue,oldValue){
      console.log("----------watch-----",newValue,oldValue);
      if(newValue==100){
        setTimeout(()=>{
           this.uploadPartCount=0
        this.uploadFlag=false
        this.$refs.uploadFile.clearFiles()
        },1000) //有的文件小,进度条太快了,看不到效果,因此加了个定时
      }
    }
  },
  methods: {
    /**
     * @description el-upload onchange 数字员工列表文件上传-初始化
     * @param {*} File 文件
     * @return {*}
     */
    async changeUpload(File) {
      this.uploadPartCount = 0 //上传成功的切片数量
      this.uploadFlag = true //正在上传,用于展示进度条
      this.loadingFile = true //上传按钮的loading
      let params = {
        fileHash: Base64.stringify(Sha256(File.raw)),
        fileName: File.name,
        totalSize: File.size
      }
      const { data = {} } = await uploadInit(params) //初始化从后台获取分片数量
      this.partCount = data.partCount
      this.loadingFile = false //这里方便测试,把这个状态置为false写在这
     let chunkList = this.sliceFile(File, data) //文件分片处理
      let groupList = this.groupChunkList(chunkList) //对分片文件数组进行分组
      await this.uploadPartFile(groupList) //文件上传
    },
    /**
     * @description 异步分片上传文件
     * @param {*} list 分片的formData对象数组
     * @return {*}
     */
    async uploadPartFile(list) {
      for (let i = 0; i < list.length; i++) {
        let itemList = Array.isArray(list[i]) ? list[i] : []
        let itemHttpUpload = itemList.map((item, index) => {
          return new Promise(async (resolve, reject) => {
            uploadPart(item)
              .then((res) => {
                if (res.code !== 200) {
                  console.log(`第${item.get('index')}片上传失败,请重传`, res)
                  reject(item)
                } else {
                    this.uploadPartCount++
                  console.log(this.uploadPartCount,`第${item.get('index')}片上传成功啦!`, res)
                  resolve(item)
                }
              })
              .catch((err) => {
                console.log('【文件异步上传报错】', err)
              })
          })
        })
        // Promise.allSettled  等待上一组 upload http请求所有都完成后 返回结果
        const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
          .then((res) => {
            res.forEach((item) => {
              console.log(item.status)
            })
            return Promise.resolve(res)
          })
          .catch((err) => {
            return Promise.resolve(err)
          })
        // 将上传失败的分片文件存储在errorChunkList
        let errorChunkList = lastGroupUpload
          .filter((item) => item.status === 'rejected')
          .map((item) => item.reason)
        // 上传失败的分片再次上传
        if (errorChunkList.length > 0) {
          errorChunkList.forEach((item) => {
            this.errorChunkUpload(item)
          })
        }
      }
      return true
    },

    /**
     * @description 针对上传失败的切片再次上传
     * @param {*}  chunk 文件上传formData参数
     * @return {*}
     */
    async errorChunkUpload(chunk) {
      let res = await uploadPart(chunk)
      if (res.code && res.code !== 200) {
        console.log(`上传失败,请重传`, res)
        this.errorChunkUpload(chunk)//失败则递归不断上传该片
      } else {
        console.log(`上传成功啦!`, res)
      }
    },

    /**
     * @description 文件分片处理
     * @param {*} File 文件对象
     * @param {*} data  初始化j接口-从后台获取分片数量等参数
     * @return {*} 分片的formData对象数组
     */

    sliceFile(File, data) {
      let chunkList = []
      const { objectName, partCount, partSize } = { ...data }
      for (let i = 0; i < partCount; i++) {
        let start = i * partSize //分片开始位置
        let end = Math.min(File.size, start + partSize) //分片结束位置
        console.log("------分片",`${start}-${end}`);
        let _chunkFile = File.raw.slice(start, end)
        console.log(`第${i}片文件`, _chunkFile)
        let formData = new FormData()
        formData.append('index', i + 1) //后台分片索引从1开始
        formData.append('multipartFile', _chunkFile)
        formData.append('objectName', objectName)
        chunkList.push(formData)
      }
      return chunkList
    },

    /**
     * @description 对分片文件数组进行分组,一组一组上传,避免一次同时发起太多次请求导致服务压力
     * @param {*}
     * @return {*}
     */
    groupChunkList(chunkList) {
      let group = Math.ceil(chunkList.length / this.groupCount) //分组
      let resultList = new Array(group) // 根据组数创建一个空的 list
      chunkList.map((item, index) => {
        let i = Math.floor(index / this.groupCount) // index / count 向下取整 为组数编号
        if (Array.isArray(resultList[i])) resultList[i].push(item)
        else resultList[i] = [item]
      })
      return resultList
    }
  },
  created() {},
  mounted() {}
}
</script>
<style lang="scss" scoped>
.progress-wrapper {
  margin-left: 0.3rem;
  margin-top: 0.9rem;
  height: 0.3.5rem;
  width: 6.15rem;
  border-radius: 0.25rem;
  background-color: #f4f4f4;
  border: 3px solid #f4f4f4;
  .progress {
    background-color: aqua;
    height: 100%;
  }
}
</style>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值