大文件续传

是什么

  • 针对问题:
    在同一个请求中,要上传大量的数据,导致整个过程会比较漫长,且失败后需要重头开始上传。

  • 解决思路:
    将这个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始,这样是否可以解决大文件上传的问题呢?

  • 要求

    • 支持拆分上传文件请求
    • 支持断点续传
    • 支持显示上传进度和暂定进度
  • 技术

    • webWorker:
      是一种在后台线程中运行脚本的技术,允许开发者在不阻塞用户界面的情况下执行复杂和耗时的任务。Web Worker 提供了一个独立的执行环境,与主线程(UI 线程)隔离开来,避免了长时间运行的脚本导致的页面卡顿。

逻辑

  • 1、将上传的文件切片,并对每个切片标上记号,是哪个文件的切片是切片的那一部分(用hash实现标记)
  • 2、后端把成功的标记记录下来,上传每个文件时,都判断一下标记是否存在,如果存在不上传,如果存在就上传。
  • 3、后端对上传完整的文件切片进行拼接。

细节

webworker 处理对文件的切片

webWorker的特点

  • 独立线程:Web Worker 在独立的线程中运行,与主线程并行执行。
  • 无阻塞:由于在独立线程中运行,Web Worker 不会阻塞主线程的执行。
  • 通信机制:主线程和 Worker 线程之间通过消息传递(postMessage 和 onmessage)进行通信。
  • 受限环境:Worker 线程不能访问 DOM,也不能调用一些特定的 Web API,如 alert 和 localStorage。

基本用法

  • 监听消息事件
    1
    2
    3
    4
    5
    6
    self.addEventListener('message', async e => {
    console.log(e)
    const { file, chunkSize } = e.data
    const fileChunkList = await createFileChunk(file, chunkSize)
    await calculateChunksHash(fileChunkList)
    })
  • 监听错误事件
    1
    2
    3
    4
    5
    self.addEventListener('error', function (e) {
    console.log("%cWorker 线程 error 监听事件: ", 'color: red', e)
    // worker 线程关闭
    self.close()
    })

切片实现

  • 1、文件切片
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //第一个参数是file,第二个是切片大小
    function createFileChunk(file, chunkSize) {
    // new Promise 的基本用法:来创建一个新的 Promise 对象,该函数有两个参数:resolve 和 reject。
    //resolve 用于在异步操作成功时返回结果,reject 用于在异步操作失败时返回错误。
    return new Promise(resolve => {
    let fileChunkList = []
    let cur = 0
    while (cur < file.size) {
    fileChunkList.push({ chunkFile: file.slice(cur, cur + chunkSize) })
    cur += chunkSize
    }
    resolve(fileChunkList)
    })
    }

切片标记

  • 每个切片有自己的MD5哈希值,所有切片的哈希值保存在SparkMD5.ArrayBuffer 实例spark中,在切片完成后,spark值传递给fileHash,webWork将fileHash和fileChunkList文件切片传给了主线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 记载并计算文件切片的 md5
async function calculateChunksHash(fileChunkList = []) {
// 创建一个 SparkMD5.ArrayBuffer 实例,用于计算 MD5 哈希值。
const spark = new SparkMD5.ArrayBuffer()

// 计算切片进度
let percentage = 0

try {
const fileHash = await loadNext()
self.postMessage({ percentage: 100, fileHash, fileChunkList })
self.close()
} catch (err) {
self.postMessage({ name: 'error', data: err })
self.close()
}

// 递归函数,处理文件的切片
async function loadNext(index = 0) {
// 所有的切片都已处理完毕
if (index >= fileChunkList.length) {
// 返回这个文件的MD5哈希值
return spark.end()
}
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsArrayBuffer(fileChunkList[index].chunkFile)
reader.onload = (e) => {
spark.append(e.target.result)
percentage += 100 / fileChunkList.length
self.postMessage({
percentage
})
resolve(loadNext(index + 1))
}
reader.onerror = (err) => reject(err)
})
}
}

主线程任务

  • 该方法能依次上传多个文件。
  • 切片大小设置为5MB
  • 多个文件输入处理逻辑:
    • 如果输入框有change事件,判断是否有文件传入,如果有将每个文件进行切片处理,并上传
    • 对每个文件绑定响应式对象,对象包括文件名、文件大小、文件状态、所有需要上传的切片、文件hash、最大报错次数、上传进度等属性。
    • 根据文件的状态来确定,当前文件需要进行的流程。
    • 需要切片上传的文件,交给webworker切片,并将结果返回。作为参数传给开始上传函数
    • 多个文件并行上传
  • 开始上传文件的逻辑:
    • 需要参数,file,inTaskArrItem(初始化的一个对象,包含单个文件的属性), fileChunkList(单个文件的切片)
    • 判断文件是否已经存在于服务器,如果存在直接return,如果不存在执行上传的逻辑
    • 将每个切片的信息写入inTaskArrItem的allChunList(所有需要上传的切片)中
    • 对allChunkList进行过滤,过滤掉已经上传成功的切片(判断是否上传成功,由checkfile函数完成)
    • 如果没有需要上传的切片,但是前面判断服务器中没有此文件,说明需要合并,再次执行文件合并函数
  • 每一个文件的处理逻辑
    • 如果没有需要上传的切片或者状态为正在被上传,不做处理
    • 找出需要上传的文件,并将入待处理文件的列表中。根据列表长度计算每个文件的并发请求数量。(chrome浏览器同域名同一时间请求的最大并发数限制为 6,如果有三个文件需要上传,那么每个文件上传只能同时并发2个请求)
    • 从需要上传的切片尾部拿2个切片,放入请求数组中。并将其从allChunkList中删除
    • 开始上传切片
  • 切片上传逻辑
    • 首先判断文件状态,如果文件状态为暂停或终端就什么都不错
    • 每个切片由三次机会,如果三次都失败,则文件上传失败