前端大文件上传实现切片上传 + 断点续传 + 秒传 + 暂停上传 + 恢复上传

前情提要:

在前端无论是Vue还是React技术栈,都离不开上传业务代码

一般情况下,前端上传文件就是new FormData,然后把文件 append 进去,然后post发送给后端就完事了,但是大文件可不能这么搞,因为美元办法预料到各种意外事件,用户浏览器退出,网络情况不好,网络断开等等,会造成我们的上传直接清零,这种在大文件,网络情况慢,传输时间长的条件下十分容易出现那我们应该如何去优化和解决这个问题呢

对于大文件上传了,确保能让用户在上传的时候想停就停(暂停功能),就算断网了也能继续接着上传(断点上传),如果是之前上传过这个文件了(服务器还存着),就不需要做二次上传了(秒传)

相关知识:浏览器同一域名只能同时请求6个。

一、Web Worker(用于分片)

Web Worker 是一种在Web开发中用于创建多线程环境的API,使得JavaScript代码能够在后台线程中执行,从而不会阻塞主线程的运行。它主要用于处理耗时任务,比如复杂的计算、数据处理等,以提高Web应用的性能和响应速度。所以我们需要采用Web Worker来实现分片。

核心代码:Blob.prototype.slice

文件分片和 Spark-MD5 通常结合使用,用于实现大文件的分片上传和校验,保证数据完整性。所以这边需要借助于spark-md5

Spark-MD5 介绍:它是一个快速的 MD5 算法实现,适用于浏览器和 Node.js 环境。结合文件分片,Spark-MD5 主要用于计算文件或分片的 MD5 校验和,确保数据在传输过程中的完整性。

直接操作

const worker = new Worker(
    new URL('@/worker/hash-worker.js', import.meta.url)
)

二、上传进度

不严谨版本:我们可以采用常规的onUploadPrigress方法可以实现接口请求进度,可以通过(e.loaded / e.total) * 100)去计算。但是大文件会有些不适用,出现100%也有可能上传失败
严谨版本:服务器返回上传成功的分片数

三、切片上传

vue文件代码

  methods: {
    handleClick() {
      // 点击触发选取文件
      this.$refs.fileInput.dispatchEvent(new MouseEvent('click'))
    },
    async hanldeUploadFile() {
      const fileEle = this.$refs.fileInput

      // 如果没有文件内容
      if (!fileEle || !fileEle.files || fileEle.files.length === 0) {
        return false
      }
      const files = fileEle.files
      // 多文件
       Array.from(files).forEach(async (item, i) => {
        const file = item
        // 单个上传文件
        let inTaskArrItem = {
          id: new Date() + i, // 因为forEach是同步,所以需要用指定id作为唯一标识
          state: 0, // 0不做任何处理,1是计算hash中,2是正在上传中,3是上传完成,4是上传失败,5是上传取消
          fileHash: '',
          fileName: file.name,
          fileSize: file.size,
          allChunkList: [], // 所有请求的数据
          whileRequests: [], // 正在请求中的请求个数,目前是要永远都保存请求个数为6
          finishNumber: 0, //请求完成的个数
          errNumber: 0, // 报错的个数,默认是0个,超多3个就是直接上传中断
          percentage: 0, // 单个文件上传进度条
        }
        this.uploadFileList.push(inTaskArrItem)

        // 开始处理解析文件
        inTaskArrItem.state = 1

        if (file.size === 0) {
          // 文件大小为0直接取消该文件上传
          this.uploadFileList.splice(i, 1)
          // 继续执行for循环
        }
        // 计算文件hash
        const { fileHash, fileChunkList } = await this.useWorker(file)

        console.log(fileHash, '文件hash计算完成')

        // 解析完成开始上传文件
        let baseName = ''
        // 查找'.'在fileName中最后出现的位置
        const lastIndex = file.name.lastIndexOf('.')
        // 如果'.'不存在,则返回整个文件名
        if (lastIndex === -1) {
          baseName = file.name
        }
        // 否则,返回从fileName开始到'.'前一个字符的子串作为文件名(不包含'.')
        baseName = file.name.slice(0, lastIndex)

        // 这里要注意!可能同一个文件,是复制出来的,出现文件名不同但是内容相同,导致获取到的hash值也是相同的
        // 所以文件hash要特殊处理
        inTaskArrItem.fileHash = `${fileHash}${baseName}`
        inTaskArrItem.state = 2
        // 初始化需要上传所有切片列表
        inTaskArrItem.allChunkList = fileChunkList.map((item, index) => {
          return {
            // 总文件hash
            fileHash: `${fileHash}${baseName}`,
            // 总文件size
            fileSize: file.size,
            // 总文件name
            fileName: file.name,
            index: index,
            // 切片文件本身
            chunkFile: item.chunkFile,
            // 单个切片hash,以 - 连接
            chunkHash: `${fileHash}-${index}`,
            // 切片文件大小
            chunkSize: this.chunkSize,
            // 切片个数
            chunkNumber: fileChunkList.length,
            // 切片是否已经完成
            finish: false,
          }
        })
        //  逐步对单个文件进行切片上传
        this.uploadSignleFile(inTaskArrItem)
      })
      console.log(this.uploadFileList, 'uploadFileList')
    },

    // 生成文件 hash(web-worker)
    useWorker(file) {
      return new Promise((resolve) => {
        const worker = new Worker(
          new URL('@/worker/hash-worker.js', import.meta.url),
          {
            type: 'module',
          }
        )
        worker.postMessage({ file, chunkSize: this.chunkSize })
        worker.onmessage = (e) => {
          const { fileHash, fileChunkList } = e.data
          if (fileHash) {
            resolve({
              fileHash,
              fileChunkList,
            })
          }
        }
      })
    },
  },

work文件代码

import SparkMD5 from './spark-md5.min.js'
// 创建文件切片
function createFileChunk(file, chunkSize) {
  return new Promise((resolve, reject) => {
    let fileChunkList = []
    let cur = 0
    while (cur < file.size) {
      // Blob 接口的 slice() 方法创建并返回一个新的 Blob 对象,该对象包含调用它的 blob 的子集中的数据。
      fileChunkList.push({ chunkFile: file.slice(cur, cur + chunkSize) })
      cur += chunkSize
    }
    // 返回全部文件切片
    resolve(fileChunkList)
  })
}

// 加载并计算文件切片的MD5
async function calculateChunksHash(fileChunkList) {
  // 初始化脚本
  const spark = new SparkMD5.ArrayBuffer()

  // 计算切片进度(拓展功能,可自行添加)
  let percentage = 0
  // 计算切片次数
  let count = 0

  // 递归函数,用于处理文件切片
  async function loadNext(index) {
    if (index >= fileChunkList.length) {
      // 所有切片都已处理完毕
      return spark.end() // 返回最终的MD5值
    }

    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.readAsArrayBuffer(fileChunkList[index].chunkFile)
      reader.onload = (e) => {
        count++
        spark.append(e.target.result)

        // 更新进度并处理下一个切片
        percentage += 100 / fileChunkList.length
        self.postMessage({ percentage }) // 发送进度到主线程

        resolve(loadNext(index + 1)) // 递归调用,处理下一个切片
      }
      reader.onerror = (err) => {
        reject(err) // 如果读取错误,则拒绝Promise
      }
    })
  }

  try {
    // 开始计算切片
    const fileHash = await loadNext(0) // 等待所有切片处理完毕
    self.postMessage({ percentage: 100, fileHash, fileChunkList }) // 发送最终结果到主线程
    self.close() // 关闭Worker
  } catch (err) {
    self.postMessage({ name: 'error', data: err }) // 发送错误到主线程
    self.close() // 关闭Worker
  }
}

// 监听消息
self.addEventListener(
  'message',
  async (e) => {
    try {
      const { file, chunkSize } = e.data
      const fileChunkList = await createFileChunk(file, chunkSize) // 创建文件切片
      await calculateChunksHash(fileChunkList) // 等待计算完成
    } catch (err) {
      // 这里实际上不会捕获到calculateChunksHash中的错误,因为错误已经在Worker内部处理了
      // 但如果未来有其他的异步操作,这里可以捕获到它们
      console.error('worker监听发生错误:', err)
    }
  },
  false
)

// 主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error事件。
self.addEventListener('error', function (event) {
  console.log('Worker触发主线程的error事件:', event)
  self.close() // 关闭Worker
})

秒传

原理:在上传文件之前先询问这个文件服务器端是否已存在,若已存在,则不需要上传了,直接让前端显示“已上传成功!”

断点上传

原理:询问后端这个文件我给你上传了多少切片,然后在前端把剩下的切片上传一下,然后再调用一下文件合并接口。

暂停上传

我们需要用到axios的AbortController方法去取消接口请求,或者使用cancelToekn方法也可以。

继续/恢复上传

原理:继续开始把剩下的分片的传给后台即可。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
前端文件上传指的是上传文件大小较大,超过了常规的文件上传限制(例如2MB)。这时需要使用特殊的上传方式,常见的有两种: 1. 分片上传:将大文件分成多个小文件,分别上传到服务器,并在服务器端将这些小文件合并成一个完整的文件。 2. 断点续传:将大文件分成多个小文件,分别上传到服务器,上传过程中若出现网络中断或其他原因导致上传失败,可以从失败的位置继续上传,避免重新上传整个文件实现前端文件上传断点续传需要使用一些第三方库或框架,例如: 1. Plupload:一个基于Flash和HTML5的文件上传库,支持分片上传断点续传。 2. Resumable.js:一个基于HTML5的文件上传库,支持断点续传。 3. jQuery-File-Upload:一个基于jQuery的文件上传插件,支持分片上传断点续传。 在使用这些库或框架实现文件上传断点续传时,需要注意以下几点: 1. 服务器端需要支持分片上传断点续传,否则无法实现这些功能。 2. 分片上传断点续传需要对文件进行切片,这可能会影响上传速度和文件完整性,需要做好相应的处理。 3. 断点续传需要记录上传进度,以便在上传失败时能够从失败的位置继续上传。 总之,前端文件上传断点续传是一个比较复杂的问题,需要使用一些专门的库或框架来实现,同时需要注意一些细节问题,才能保证上传效率和上传成功率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ifHappyEveryDay@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值