前端大文件切片上传,断点续传、秒传等解决方案,vue中使用实例
  9m65el8SCpbP 2023年11月30日 21 0

先看逻辑

如何切片?如何获取文件唯一hash?与后端交互获取文件上传的状态,用于判断情况,是秒传还是续传?上传切片文件,判断失败文件重新执行?全部上传完成通知后端?

1、先上全部代码,后面第2部分解析、第3部分vue中使用
相关依赖
spark-md5主要用于拿取文件的md5
mitt 发布订阅

import SparkMD5 from 'spark-md5'
import { getStartMultipartUpload, postUploadMultipartPart, postCompleteMultipartUpload } from '@/api/file'
import mitt from '@/utils/mitt'class UpLoadVideo {
  /**
   * file 流文件
   * sectionSize 切片大小(MB) 默认 5MB
   * failCount 失败重试次数 默认 3次
   * concurrencyMax 最大并发量
   * @param {*} params
   */
  constructor(params) {
    // file流
    this.file = params.file
    this.sectionSize = params.sectionSize || 5
    this.failCount = params.failCount || 3
    this.concurrencyMax = params.concurrencyMax || 1
    // 是否停止上传
    this.isStop = false
    this._count = 0
    // 每个文件在上传接口检查前都会返回的上传id  md5一样的话  返回的 id都是一样的
    this.uploadId = ''
    // md5 情况
    this.md5 = {
      value: '',
      progress: 0
    }
    // 上传进度
    this.upProgress = 0
    // 0 未开始  1 切片中  2 切片完成 3 开始上传中  4 全部上传完成 5 上传失败 前端定义的状态
    this.status = 0
    // 发布订阅
    this.mitt = mitt
    // 切片list
    this.fileList = []
    // 最开始的切片数量
    this.multipartCount = 0
    // 视频时长(毫秒)本地获取
    this.duration = 0
    this.upVideo()
  }
  /**
   * 获取文件md5以及切片
   * 以及检查md5的状态  是否已经有上传 或者 上传到一半
   * @returns
   */
  async upVideo() {
    const file = this.file
    this.mitt.emit('currentFunc', { msg: '正在检查文件信息' })
    const md5 = await this.getMd5(2)
    const duration = await this.getDuration(file)
    this.duration = duration
    const size = 1024 * 1024 * this.sectionSize // 切片大小
    const fileList = []
    let index = 0 // 切片序号
    for (let cur = 0; cur < file.size; cur += size) {
      const sectionFile = file.slice(cur, cur + size)
      const partIdx = ++index
      fileList.push({
        partIdx,
        multipartName: file.name + '_' + partIdx,
        file: sectionFile,
        size: sectionFile.size
      })
    }
    this.multipartCount = index
    const res = await this.getDetail({
      MD5: md5,
      Size: file.size,
      FileName: file.name,
      MultipartCount: fileList.length
    })
    // 切片数组
    const newList = []
    // 根据接口返回的数据出来  partIdxList里面表示那些索引表示已上传 筛选出未上传的
    if (res.partIdxList && res.partIdxList.length) {
      fileList.forEach(item => {
        if (!res.partIdxList.includes(item.partIdx)) {
          newList.push(item)
        }
      })
    }
    this.uploadId = res.uploadId
    // state 2 已上传
    if (res.state === 2) {
      this.status = 4
      this.upEmit('currentFunc', { msg: '上传完成', ...res })
      return
    }
    // state 1 上传中  并且 切片长度等于已上传序号列表的长度
    if (res.state === 1 && fileList.length === res.partIdxList.length) {
      this.getCompleteMultipartUpload(false)
      return
    }
    this.fileList = newList.length ? newList : fileList
    this.upSection()
  }
  // 上传前的查询
  async getDetail(data) {
    return new Promise((resolve, reject) => {
      getStartMultipartUpload(data).then(res => {
        resolve(res.data)
      })
    })
  }
  /**
   * 是否检查
   * @param {*} is
   */
  async getCompleteMultipartUpload(is = false) {
    const formData = new FormData()
    formData.append('UploadId', this.uploadId)
    postCompleteMultipartUpload(formData).then(res => {
      this.upProgress = 100
      this.status = 4
      this.upEmit('currentFunc', { msg: '上传完成', ...res.data })
    })
  }
  async upSection(fileList) {
    if (this._count === this.failCount) {
      this.upProgress = 0
      this.mitt.emit('currentFunc', { msg: '上传失败,请重新上传试试,会保留您此次的上传进度,下次上传将会加速上传' })
      return
    }
    fileList = fileList || this.fileList
    console.time()
    if (fileList.length === 0) {
      console.timeEnd()
      this.getCompleteMultipartUpload(true)
      return
    }
    const pool = []// 并发池
    const max = this.concurrencyMax // 最大并发量
    let finish = 0// 完成的数量
    const failList = []// 失败的列表
    const upProgress = this.upProgress
    if (!upProgress) {
      // 延时1秒执行 并且完成的数量的大于0
      setTimeout(() => {
        if (!finish) {
          this.mitt.emit('currentFunc', { msg: '准备上传...' })
        }
      }, 1000)
    }
    this.status = 3
    for (let i = 0; i < fileList.length; i++) {
      const item = fileList[i]
      // 调用接口,上传切片
      const task = this.apiFun(item).then(res => {
        const progress = (100 / fileList.length * finish)
        this.upProgress = Math.floor(progress * 100) / 100
        this.upEmit('currentFunc', { msg: '正在上传文件' + this.upProgress + '%' })
        // 请求结束后将该Promise任务从并发池中移除
        const index = pool.findIndex(t => t === task)
        pool.splice(index)
      }).catch(_ => {
        // 失败的存入失败数组
        failList.push(item)
      }).finally(_ => {
        finish++
        // 所有请求都请求完成后 检查失败的数组
        if (finish === fileList.length) {
          // 检查失败次数
          this._count++
          this.upSection(failList)
        }
      })
      pool.push(task)
      if (pool.length === max) {
        // 结束上传
        if (this.isStop) break
        // 每当并发池跑完一个任务,就再塞入一个任务
        await Promise.allSettled(pool)
      }
    }
  }  // 切片请求上传
  apiFun(item) {
    return new Promise((resolve, reject) => {
      const formData = new FormData()
      formData.append('UploadId', this.uploadId)
      formData.append('MultipartName', item.multipartName)
      formData.append('PartIdx', item.partIdx)
      formData.append('Size', item.size)
      formData.append('File', item.file)
      postUploadMultipartPart(formData).then(res => {
        resolve(item.partIdx)
      }).catch(err => {
        reject(err)
      })
    })
  }
  upEmit(event = 'currentFunc', data) {
    this.mitt.emit(event, { ...data, md5: this.md5, upProgress: this.upProgress, status: this.status, duration: this.duration })
  }
  /**
   *
   * @param {*} computeCount 计算次数,不传默认全部切片计算
   * @returns
   */
  getMd5(computeCount) {
    const that = this
    return new Promise((resolve, reject) => {
      // 兼容
      const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
      // 切片计算大小
      const chunkSize = 2097152 // Read in chunks of 2MB
      // 当前file流
      const file = that.file
      // 当前可以分多少切片
      let chunks = Math.ceil(file.size / chunkSize)
      // 传入的次数小于 切片次数 就使用传入的
      if (computeCount && computeCount < chunks) {
        chunks = computeCount
      }
      let currentChunk = 0
      // 计算md5的库方法
      const spark = new SparkMD5.ArrayBuffer()
      // 创建FileReader
      const fileReader = new FileReader()
      // 写入切片流
      function loadNext() {
        const start = currentChunk * chunkSize
        const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
        fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
      }
      loadNext()
      // 写入完成后计算md5
      fileReader.onload = function(e) {
        spark.append(e.target.result) // Append array buffer
        currentChunk++
        if (currentChunk < chunks) {
          const progress = ((100 / chunks) * currentChunk)
          this.status = 1
          that.md5 = {
            value: '',
            progress: Math.floor(progress * 100) / 100
          }
          that.upEmit('currentFunc', { msg: '正在检查文件信息' + that.md5.progress + '%' })
          loadNext()
        } else {
          const md5 = {
            value: spark.end(),
            progress: 100
          }
          that.md5 = md5
          this.status = 2
          that.upEmit('currentFunc', { msg: '检查文件信息100%' })
          resolve(md5.value)
        }
      }      fileReader.onerror = function() {
        that.upEmit('currentFunc', { msg: '检查文件信息失败,请重新试试' })
      }
    })
  }
  /**
   * 获取视频长度
   * @param {*} file
   * @returns
   */
  getDuration(file) {
    return new Promise((resolve, reject) => {
      const video = document.createElement('video')
      video.preload = 'metadata'
      video.src = URL.createObjectURL(file)
      video.onloadedmetadata = function() {
        window.URL.revokeObjectURL(video.src)
        const duration = video.duration
        resolve(Math.floor(duration * 1000))
      }
    })
  }  /**
   *停止上传
   */
  stopUpLoad() {
    this.upProgress = 0
    this.status = 5
    this.isStop = true
    this.mitt.emit('currentFunc', { msg: '上传失败,请重新试试' })
    this.destroy()
  }  destroy() {
    this.file = null
    this.mitt.clear()
  }
}export default UpLoadVideo

2代码解析(函数)
整个上传用的 阿里oss
后台封装了3个api 第一个是上传前的查询、第二个上传视频分片、第三个完成上传

import { getStartMultipartUpload, postUploadMultipartPart, postCompleteMultipartUpload } from '@/api/file'
1


2.1 getMd5用于获取文件的md5,我这边只拿取前几个切片数量的作为全局的md5,因为大文件的情况下,获取文件md5也是耗时的操作
2.2 getDuration获取视频时长的
2.3 upVideo每个切片的大小的处理、上传前的查询,主要处理是否已经上传
2.3.1 切片的大小的处理

const size = 1024 * 1024 * this.sectionSize // 切片大小
    const fileList = []
    let index = 0 // 切片序号
    for (let cur = 0; cur < file.size; cur += size) {
      const sectionFile = file.slice(cur, cur + size)
      const partIdx = ++index
      fileList.push({
        partIdx, // 后续用于判断那个切片已经上传
        multipartName: file.name + '_' + partIdx,
        file: sectionFile,
        size: sectionFile.size
      })
    }



2.3.2 上传前的查询

const res = await this.getDetail({
      MD5: md5,
      Size: file.size,
      FileName: file.name,
      MultipartCount: fileList.length
    })
    // 切片数组
    const newList = []
    // 根据接口返回的数据出来  partIdxList里面表示那些索引表示已上传 筛选出未上传的
    if (res.partIdxList && res.partIdxList.length) {
      fileList.forEach(item => {
        if (!res.partIdxList.includes(item.partIdx)) {
          newList.push(item)
        }
      })
    }
    this.uploadId = res.uploadId
    // state 2 已上传 直接返回结果
    if (res.state === 2) {
      this.status = 4
      this.upEmit('currentFunc', { msg: '上传完成', ...res })
      return
    }
	// state 1 上传中  并且 切片长度等于已上传序号列表的长度  直接调用完成上传接口
    if (res.state === 1 && fileList.length === res.partIdxList.length) {
      this.getCompleteMultipartUpload(false)
      return
    }
    // 开始上传
	 this.fileList = newList.length ? newList : fileList
    this.upSection()

2.4 upSection 开始分片上传

if (this._count === this.failCount) {
      this.upProgress = 0
      this.mitt.emit('currentFunc', { msg: '上传失败,请重新上传试试,会保留您此次的上传进度,下次上传将会加速上传' })
      return
    }
    fileList = fileList || this.fileList
    console.time()
    // 全部上传完成 调用完成接口
    if (fileList.length === 0) {
      console.timeEnd()
      this.getCompleteMultipartUpload(true)
      return
    }
    const pool = []// 并发池
    const max = this.concurrencyMax // 最大并发量
    let finish = 0// 完成的数量
    const failList = []// 失败的列表
    const upProgress = this.upProgress
    if (!upProgress) {
      // 延时1秒执行 并且完成的数量的大于0
      setTimeout(() => {
        if (!finish) {
          this.mitt.emit('currentFunc', { msg: '准备上传...' })
        }
      }, 1000)
    }
    this.status = 3
    for (let i = 0; i < fileList.length; i++) {
      const item = fileList[i]
      // 调用接口,上传切片
      const task = this.apiFun(item).then(res => {
        const progress = (100 / fileList.length * finish)
        this.upProgress = Math.floor(progress * 100) / 100
        this.upEmit('currentFunc', { msg: '正在上传文件' + this.upProgress + '%' })
        // 请求结束后将该Promise任务从并发池中移除
        const index = pool.findIndex(t => t === task)
        pool.splice(index)
      }).catch(_ => {
        // 失败的存入失败数组 每次fileList执行完的话  会执行failList的数组
        failList.push(item)
      }).finally(_ => {
        finish++
        // 所有请求都请求完成后 检查失败的数组
        if (finish === fileList.length) {
          // 检查失败次数
          this._count++
          this.upSection(failList)
        }
      })
      pool.push(task)
      if (pool.length === max) {
        // 结束上传
        if (this.isStop) break
        // 每当并发池跑完一个任务,就再塞入一个任务
        await Promise.allSettled(pool)
      }
    }

3、vue中使用封装好的js
vue文件中引入

import UpLoadVideo from '@/utils/uploadVideo'
data(){  
	return {
	 uploadVideo: null
	}
 }
//  fileBase 就是 流文件
   let newUploadVideo = new UpLoadVideo({
        file: fileBase
   })
	 newUploadVideo.mitt.on('currentFunc', data => {
        this.loadingText = data.msg // 一些上传中的提示
        this.upProgress = data.upProgress  // 上传进度
        // status是自定义的  state是后台返回的
        if (data.status === 4 || data.state === 2) {
            console.log('%c [ data 中数据就是接口返回的或者封装js里返回的 ]', 'font-size:13px; background:pink; color:#bf2c9f;', data)
          this.upProgress = 0
          this.loadingText = ''
          newUploadVideo.destroy()
          newUploadVideo = null
          this.uploadVideo = null
        }
      })
      this.uploadVideo = newUploadVideo

参考文章:http://blog.ncmem.com/wordpress/2023/11/01/前端大文件切片上传,断点续传、秒传等解决方案/


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月30日 0

暂无评论

推荐阅读
9m65el8SCpbP