松鼠的博客

导航

vue 大文件分片上传(断点续传、并发上传、秒传)

对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。

本文是基于 springboot + vue 实现的文件上传,本文主要介绍vue实现文件上传的步骤及代码实现

 

上传分步:

本人分析上传总共分为:

  • MD5读取文件,获取文件的MD5编码
  • 请求服务端判断文件是否上传,如上传完成就直接返回文件地址
  • 如未上传,判断是否是断点续传
  • 判断是并发上传还是顺序上传
  • 开始分片文件上传,分片上传完成后写入已上传列表中
  • 判断是否上传完成

直接上代码

文件上传:

import md5 from 'js-md5' //引入MD5加密
import UpApi from '@/api/common.js'
import { concurrentExecution } from '@/utils/jnxh'
 
/**
 * 文件分片上传
 * @params file {File} 文件
 * @params pieceSize {Number} 分片大小 默认3MB
 * @params concurrent {Number} 并发数量 默认2
 * @params process {Function} 进度回调函数
 * @params success {Function} 成功回调函数
 * @params error {Function} 失败回调函数
 */
export const uploadByPieces = ({
                                 file,
                                 pieceSize = 3,
                                 concurrent = 3,
                                 success,
                                 process,
                                 error
                               }) => {
  // 如果文件传入为空直接 return 返回
  if (!file || file.length < 1) {
    return error('文件不能为空')
  }
  let fileMD5 = '' // 总文件列表
  const chunkSize = pieceSize * 1024 * 1024 // 1MB一片
  const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
  const chunkList = [] // 分片列表
  let uploaded = [] // 已经上传的
  let fileType = '' // 文件类型
  // 获取md5
  /***
   * 获取md5
   **/
  const readFileMD5 = () => {
    // 读取视频文件的md5
    fileType = file.name.substring(file.name.lastIndexOf('.') + 1, file.name.length)
    console.log('获取文件的MD5值')
    let fileRederInstance = new FileReader()
    console.log('file', file)
    fileRederInstance.readAsBinaryString(file)
    fileRederInstance.addEventListener('load', e => {
      let fileBolb = e.target.result
      fileMD5 = md5(fileBolb)
      var index = file.name.lastIndexOf('.')
      var tp = file.name.substring(index + 1, file.name.length)
      let form = new FormData()
      form.append('filename', file.name)
      form.append('identifier', fileMD5)
      form.append('objectType', fileType)
      form.append('chunkNumber', 1)
      UpApi.uploadChunk(form).then(res => {
        if (res.skipUpload) {
          console.log('文件已被上传')
          success && success(res)
        } else {
          // 判断是否是断点续传
          if (res.uploaded && res.uploaded.length != 0) {
            uploaded = [].concat(res.uploaded)
          }
          console.log('已上传的分片:' + uploaded)
          // 判断是并发上传或顺序上传
          if (concurrent == 1 || chunkCount == 1) {
            console.log('顺序上传')
            sequentialUplode(0)
          } else {
            console.log('并发上传')
            concurrentUpload()
          }
        }
      }).catch((e) => {
        console.log('文件合并错误')
        console.log(e)
      })
    })
  }
  /***
   * 获取每一个分片的详情
   **/
  const getChunkInfo = (file, currentChunk, chunkSize) => {
    let start = currentChunk * chunkSize
    let end = Math.min(file.size, start + chunkSize)
    let chunk = file.slice(start, end)
    return {
      start,
      end,
      chunk
    }
  }
  /***
   * 针对每个文件进行chunk处理
   **/
  const readChunkMD5 = () => {
    // 针对单个文件进行chunk上传
    for (var i = 0; i < chunkCount; i++) {
      const {
        chunk
      } = getChunkInfo(file, i, chunkSize)
 
      // 判断已经上传的分片中是否包含当前分片
      if (uploaded.indexOf(i + '') == -1) {
        uploadChunk({
          chunk,
          currentChunk: i,
          chunkCount
        })
      }
    }
  }
  /***
   * 原始上传
   **/
  const uploadChunk = (chunkInfo) => {
    var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
    console.log(sd, '进度')
    process(sd)
    console.log(chunkInfo, '分片大小')
    let inde = chunkInfo.currentChunk + 1
    if (uploaded.indexOf(inde + '') > -1) {
      const {
        chunk
      } = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
      uploadChunk({
        chunk,
        currentChunk: inde,
        chunkCount
      })
    } else {
      var index = file.name.lastIndexOf('.')
      var tp = file.name.substring(index + 1, file.name.length)
      // 构建上传文件的formData
      let fetchForm = new FormData()
      fetchForm.append('identifier', fileMD5)
      fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
      fetchForm.append('chunkSize', chunkSize)
      fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
      const chunkfile = new File([chunkInfo.chunk], file.name)
      fetchForm.append('file', chunkfile)
      // fetchForm.append('file', chunkInfo.chunk)
      fetchForm.append('filename', file.name)
      fetchForm.append('relativePath', file.name)
      fetchForm.append('totalChunks', chunkInfo.chunkCount)
      fetchForm.append('totalSize', file.size)
      fetchForm.append('objectType', tp)
      // 执行分片上传
      let config = {
        headers: {
          'Content-Type': 'application/json',
          'Accept': '*/*'
        }
      }
 
      UpApi.uploadChunk(fetchForm, config).then(res => {
 
        if (res.code == 200) {
          console.log('分片上传成功')
          uploaded.push(chunkInfo.currentChunk + 1)
          // 判断是否全部上传完
          if (uploaded.length == chunkInfo.chunkCount) {
            console.log('全部完成')
            success(res)
            process(100)
          } else {
            const {
              chunk
            } = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
            uploadChunk({
              chunk,
              currentChunk: chunkInfo.currentChunk + 1,
              chunkCount
            })
          }
 
        } else {
          console.log(res.msg)
        }
 
      }).catch((e) => {
        error && error(e)
      })
      // if (chunkInfo.currentChunk < chunkInfo.chunkCount) {
      //   setTimeout(() => {
      //
      //   }, 1000)
      // }
    }
  }
  /***
   * 顺序上传
   **/
  const sequentialUplode = (currentChunk) => {
    const {
      chunk
    } = getChunkInfo(file, currentChunk, chunkSize)
    let chunkInfo = {
      chunk,
      currentChunk,
      chunkCount
    }
    var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
    process(sd)
    console.log('当前上传分片:' + currentChunk)
    let inde = chunkInfo.currentChunk + 1
    if (uploaded.indexOf(inde + '') > -1) {
      console.log('分片【' + currentChunk + '】已上传')
      sequentialUplode(currentChunk + 1)
    } else {
      let uploadData = createUploadData(chunkInfo)
      let config = {
        headers: {
          'Content-Type': 'application/json',
          'Accept': '*/*'
        }
      }
      // 执行分片上传
      UpApi.uploadChunk(uploadData, config).then(res => {
        if (res.code == 200) {
          console.log('分片【' + currentChunk + '】上传成功')
          uploaded.push(chunkInfo.currentChunk + 1)
          // 判断是否全部上传完
          if (uploaded.length == chunkInfo.chunkCount) {
            console.log('全部完成')
            success(res)
            process(100)
          } else {
            sequentialUplode(currentChunk + 1)
          }
 
        } else {
          console.log(res.msg)
        }
 
      }).catch((e) => {
        error && error(e)
      })
    }
  }
  /***
   * 并发上传
   **/
  const concurrentUpload = () => {
    for (var i = 0; i < chunkCount; i++) {
      chunkList.push(Number(i))
    }
    console.log('需要上传的分片列表:' + chunkList)
    concurrentExecution(chunkList, concurrent, (curItem) => {
      return new Promise((resolve, reject) => {
        const {
          chunk
        } = getChunkInfo(file, curItem, chunkSize)
        let chunkInfo = {
          chunk,
          currentChunk: curItem,
          chunkCount
        }
        var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
        process(sd)
        console.log('当前上传分片:' + curItem)
        let inde = chunkInfo.currentChunk + 1
        if (uploaded.indexOf(inde + '') == -1) {
          // 构建上传文件的formData
          let uploadData = createUploadData(chunkInfo)
          // 请求头
          let config = {
            headers: {
              'Content-Type': 'application/json',
              'Accept': '*/*'
            }
          }
          UpApi.uploadChunk(uploadData, config).then(res => {
            if (res.code == 200) {
              uploaded.push(chunkInfo.currentChunk + 1)
              console.log('已经上传完成的分片:' + uploaded)
              // 判断是否全部上传完
              if (uploaded.length == chunkInfo.chunkCount) {
                success(res)
                process(100)
              }
              resolve()
            } else {
              reject(res)
              console.log(res.msg)
            }
 
          }).catch((e) => {
            reject(res)
            error && error(e)
          })
        } else {
          console.log('分片【' + chunkInfo.currentChunk + '】已上传')
          resolve()
        }
      })
    }).then(res => {
      console.log('finish', res)
    })
  }
  /***
   * 创建文件上传参数
   **/
  const createUploadData = (chunkInfo) => {
    let fetchForm = new FormData()
    fetchForm.append('identifier', fileMD5)
    fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
    fetchForm.append('chunkSize', chunkSize)
    fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
    const chunkfile = new File([chunkInfo.chunk], file.name)
    fetchForm.append('file', chunkfile)
    // fetchForm.append('file', chunkInfo.chunk)
    fetchForm.append('filename', file.name)
    fetchForm.append('relativePath', file.name)
    fetchForm.append('totalChunks', chunkInfo.chunkCount)
    fetchForm.append('totalSize', file.size)
    fetchForm.append('objectType', fileType)
    return fetchForm
  }
  readFileMD5() // 开始执行代码
}

并发控制:

/**
* 并发执行
* @params list {Array} - 要迭代的数组
* @params limit {Number} - 并发数量控制数,最好小于3
* @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
* @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
*/
export function concurrentExecution(list, limit, asyncHandle) {
// 递归执行
let recursion = (arr) => {
// 执行方法 arr.shift() 取出并移除第一个数据
return asyncHandle(arr.shift()).then(() => {
// 数组还未迭代完,递归继续进行迭代
if (arr.length !== 0) {
return recursion(arr)
} else {
return 'finish'
}
})
}
// 创建新的并发数组
let listCopy = [].concat(list)
// 正在进行的所有并发异步操作
let asyncList = []
limit = limit > listCopy.length ? listCopy.length : limit
console.log(limit)
while (limit--) {
asyncList.push(recursion(listCopy))
}
// 所有并发异步操作都完成后,本次并发控制迭代完成
return Promise.all(asyncList)
}

到此这篇关于vue 大文件分片上传(断点续传、并发上传、秒传)的文章就介绍到这了

 

参考文章:http://blog.ncmem.com/wordpress/2023/11/07/vue-%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%86%e7%89%87%e4%b8%8a%e4%bc%a0%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e3%80%81%e5%b9%b6%e5%8f%91%e4%b8%8a%e4%bc%a0%e3%80%81%e7%a7%92%e4%bc%a0/

欢迎入群一起讨论

 

 

posted on 2023-11-07 13:34  Xproer-松鼠  阅读(102)  评论(0编辑  收藏  举报