使用Promise实现大文件切片上传并发控制

405 阅读4分钟

在Web开发中,处理大文件上传是一个常见的需求。为了提高上传效率和用户体验,我们可以采用文件切片的方式,将大文件切割成多个小文件片段(或称为“块”或“chunk”),然后并行上传到服务器。以下是如何使用JavaScript的Promise来实现这一功能的分享笔记。

功能关键字

1、利用Promise.race + Promise.all + 递归控制最大并发为6个;
2、错误中断处理;
3、上传试探
4、上传进度展示

实现步骤

1. 文件切片

首先,使用JavaScript的File API对大文件进行切片。File.slice()方法可以用来将文件切割成多个小片段。对每个切片,附带必要的信息,如切片的索引、文件总大小、分片总数等。

  // 将文件分块
  chunkFile(file, chunkSize = 1024 * 1024 * 5) { // 切片大小5MB
    const chunks = []
    const count = Math.ceil(file.size / chunkSize)
    for (let i = 0; i < count; i++) {
      const start = i * chunkSize
      const end = Math.min(file.size, start + chunkSize)
      // 切片文件
      const chunk = file.slice(start, end)
      chunks.push({ number: i + 1, data: chunk })
    }
    return chunks
  }

2. 创建上传逻辑

接下来,为这些切片文件创建上传逻辑。使用fetchXMLHttpRequest发送POST请求,将每个切片作为请求体发送到服务器。

  // 切割包上传
  uploadChunk(chunk, currentIndex) {
    return new Promise(async(resolve, reject) => {
      // 模拟上传
      setTimeout(() => {
        if (Math.random() > 0.2) {
          console.warn('上传:切片number' + chunk.number + '|切片角标:' + currentIndex)
          resolve(`Chunk ${chunk.number} uploaded successfully`)
        } else {
          reject(`Chunk ${chunk.number} upload failed`)
        }
      }, 1000)
    })
  }

3、使用Promise控制并发 Promise.race + Promise.all + 递归

1、限制并行上传的切片数量(比如最多6个),可以通过Promise数组结合递归控制并发数量,通过Promise.race监听第一个上传成功,并及时补充切片,Promise.all监听所有切片上传完成; 2、监听每个上传请求的进度事件和错误事件。当切片上传成功时,可以移除对应的切片数据,并追加新的上传;上传失败时,终止后续上传。

  /**
   * 分片上传所有文件
   * @param chunks 分片对象
   * @returns promise
   */
  uploadLeftChunks(chunks) {
    console.log('总切片数:' + chunks.length)
    return new Promise((resolve, reject) => {
      // 控制最大并发,减小服务器压力
      const uploadFileChunksInParallel = (chunks, maxConcurrent = 6) => {
        let running = 0
        const promises = []
        let currentIndex = 0
        // 一旦有一个报错了,就不继续启动后面的上传了
        let ifFailed = false
        // 递归函数,用于启动或等待新的上传
        const startUpload = (index) => {
          currentIndex = index
          if (currentIndex >= chunks.length || ifFailed) {
            console.log('所有切片都上传完毕或有切片上传失败, 停止上传')
            // 所有切片都上传完毕
            return resolve(Promise.all(promises))
          }
          if (running < maxConcurrent) {
            console.log('小于最大并行数,继续插入请求' + currentIndex)
            console.log('当前切片:' + currentIndex)
            // 如果当前运行的上传数量小于最大并发数,则启动一个新的上传
            running++
            const promise = this.uploadChunk(chunks[currentIndex], currentIndex)
              .then(result => {
                console.log('切片上传成功')
                running--
                // 启动下一个上传
                startUpload(currentIndex + 1)
              })
              .catch(error => {
                console.log('请求失败一个,任务终止')
                ifFailed = true
                reject(error)
              })
            promises.push(promise)
          } else {
            console.log('等于最大并行数,等待第一个请求完成插入请求')
            // 否则,等待一个Promise完成后再启动新的上传
            Promise.race(promises).then(() => {
              startUpload(currentIndex)
            })
          }
        }
        // maxConcurrent, chunks.length取最小进行调用
        const times = Math.min(maxConcurrent, chunks.length)
        for (let i = 0; i < times; i++) {
          // 一次性启动启动上传
          startUpload(i)
        }
      }
      uploadFileChunksInParallel(chunks, 6)
    })
  }

4. 上传试探 + 上传完成后合并

上传试探的作用是确保文件类型符合要求,接口能正常请求,避免直接平铺请求导致的过多无效请求。当所有切片都上传完毕后,发送一个结束上传的请求给服务器,告知服务器所有切片已经上传完成,可以进行文件的重组。

  // 试探上传第一块
  await this.uploadChunk(chunks[0], 1)
  let uploadRes
  if (chunks.length > 1) {
    // 上传剩下的所有的块
    uploadRes = await this.uploadLeftChunks(chunks.slice(1))
  }
  // 处理所有块上传成功后的逻辑,例如合并文件块等
  console.log('All chunks uploaded successfully:', uploadRes)
  const { data: { path: completeUploadRes }} = await this.completeUpload()

6. 进度显示

在整个上传过程中,可以根据每个切片的上传进度来更新整体上传进度,并展示给用户。

  // 通过当前完成的任务数 / 总任务数:切片上传任务总数 + 1(合并请求任务)得到进度百分比
  finishedTaskCount = 20
  totalTaskCount = 200

总结

通过上述步骤,我们可以使用Promise实现大文件切片的并行上传,并控制同时上传的切片数量,以提高上传效率和用户体验。在实际应用中,还需要考虑错误处理、重试机制、上传进度显示等细节。