支持上传兄弟几个G的黑历史:分片上传 & ThreadPool

92 阅读6分钟

Worker补充内容

缺陷

  • 同源策略
  • 无法访问DOM 只能访问 navigator 和 locatin 对象
  • 只能是js,不能是ts,不能用本地文件,不能执行alert,confirm

优点

  • 多线程
  • 适合执行CPU密集型,不阻塞主线程

image.png

步骤

  1. 获取指定文件
  2. 按照指定大小分blob片

对File调用 ``slice 方法

  • slice 方法用于从 File 对象中提取一个新的 BlobFile 对象,该对象表示原始文件的一部分。
  • 这个方法并不会读取文件的实际内容,只是创建一个新的 BlobFile 对象,其中包含了指定范围内的数据。
  1. 分组/全部转换成 arraybuffer用于计算 分片hash

    1. 分片作用: 表示是否上传过
    2. 计算hash是cpu密集的,放入webworker
    3. 基于webWorker实现线程池加速计算Hash (md5 / crc32选择)

将blob转换为arrayBuffer的方法

使用blob.arrayBuffer() //使用blob对象本身的方法 这个方法是异步的!基于Promise

使用fileReader() fr.readAsArraybuffer() //使用了readFile API 基于事件API 所以如果使用这个,要利用一个异步函数封装一下,最后才能返回Promise.all

image.png

不放入webworker

如果你将这个过程放到 Worker 中, 由于 File 或 Blob 并不是 Worker 中的可 Transfer 对象

此处会导致 主线程与 Worker 通信时进行结构化克隆, 由此会产生额外的CPU性能消耗和内存消耗

而且如果文件很大时(大概超过2GB)会导致 Worker 线程 OOM (内存溢出错误)

实现webWorkerPool

image.png

image.png

WorkerPool 类有以下几个主要的属性和方法:

  • pool:这是一个 WorkerWrapper 对象的数组,每个 WorkerWrapper 对象都封装了一个 Web Worker 和它的状态。
  • maxWorkerCount:这是 WorkerPool 可以同时运行的最大 Worker 数量。这个数量默认为设备的硬件并发级别,如果设备的硬件并发级别无法确定,则默认为 4。
  • curRunningCount:这是一个 BehaviorSubject,表示当前正在运行的 Worker 数量。
  • results:这是一个数组,用于存储每个 Worker 的结果。
  • constructor(maxWorkers = navigator.hardwareConcurrency || 4):这是 WorkerPool 的构造函数,它接受一个可选的参数,表示最大 Worker 数量。
  • exec<T>(params: ArrayBuffer[]):这是 WorkerPool 的主要方法,它接受一个 ArrayBuffer 数组作为参数,然后在 WorkerPool 中的每个 Worker 上并行执行任务。这个方法返回一个 Promise,当所有的 Worker 都完成任务时,这个 Promise 将解析为一个包含所有结果的数组。

exec 方法中,首先清空 results 数组,然后创建一个包含所有参数和它们的索引的数组。然后,创建一个新的 Promise,并在这个 Promise 的执行器函数中,订阅 curRunningCount BehaviorSubject。每当 curRunningCount 的值发生变化时,都会检查是否有空闲的 Worker 可以用来执行新的任务,如果有,则将任务分配给这些 Worker。当所有的任务都完成时,这个 Promise 将解析为 results 数组。

PromisePool

思路和workerPool大同小异

  1. 订阅一个并发数,回调为:并发数改变的时候如果还有任务需要执行,重新计算执行列表,从任务队列中取出
  2. 遍历执行列表执行传入的异步函数,promise 结果放入列表中,并更新并发数
  3. 当任务队列空,而且当前的并发数为0,说明没有任务需要并发,返回并发的结果

使用:

import { PromisePool } from './promisePool'

// 定义一些异步任务
const tasks = [
  () => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 2'), 2000)),
  () => new Promise(resolve => setTimeout(() => resolve('Task 3'), 3000)),
]

// 创建一个 PromisePool 实例
const pool = new PromisePool(tasks, 2)

// 执行任务
pool.exec().then(results => {
  console.log(results) // 输出: ['Task 1', 'Task 2', 'Task 3']
})

断点续传和秒传

  1. 后台维护一个接收到并确认的文件序列,如 [ 1,1,1,1,0,0,1,1 ] 1 表示对应的分片已经上载完成,0表示失败或者错误
  2. 再次上传的时候,通过整个文件的md5与后台通信获取文件序列,如果序列全1,则秒传成功
  3. 如果没有则返回全0 ,文件从头开始分片上传
  4. 否则上传序列为0的部分,等待后台校验返回

计算md5时优化内存

  • 原本的方案是计算出所有分片的arrayBuffer,然后通过workerPool并发执行,这里可能会导致浏览器的内存占用严重

  • 将blob分片数组分组,每一组是并发数量个,在执行之前才计算对应的arrayBuffer ,然后串行地执行一个每一个分组,每一个分组正好能够利用所有worker线程,

知识要点

  1. File对象 继承 Blob 能够 slice

  2. webworkerTransfer : 可转移对象通常用于共享资源,该资源一次仅能安全地暴露在一个 JavaScript 线程中。例如,ArrayBuffer 是一个拥有内存块的可转移对象。当此类缓冲区(buffer)在线程之间传输时,相关联的内存资源将从原始的缓冲区分离出来,并且附加到新线程创建的缓冲区对象中。原始线程中的缓冲区对象不再可用,因为它不再拥有属于自己的内存资源了。

  3. 读取大文件:使用FileReader的时候,利用readAsArrayBuffer能够读取大文件,但是如果调用readAsText浏览器会崩溃,因为一下把所有的内容读入内存,内存爆了。

    1. 加载到浏览器中的文件File类型是一个Blob类型,他是文件的元数据
    2. 通过readFileAPI我们才一次性或者分片的将文件读入
    3.  const fs = new FileReader()
       fs.onload()=>{}
       fs.readAsText()//一次性
       fs.readAsArrayBuffer()//分片(大文件) 
      

为什么用分片和前端hash

分片原因

  1. 大文件并发上传,提高速度
  2. 断点续传
  3. 基于真实上传进度的进度条

文件指纹原因

  1. 业务方要求

image.png

踩坑

WebWorker

  1. TypeError: Failed to execute 'postMessage' on 'DedicatedWorkerGlobalScope': Overload resolution failed.

  • 原因,webworker的postMessage方法调用不正确

  • 心路历程 :根据教学文章,我将worker文件写为ts文件,在postMessage方法中,根据ts类型提示,我将他写为postMessage(data,'scope',[transferData]),我使用new Worker(new URL(string,baseURL),{type: 'module'})开启worker最后报错

  • 解决方案:

    • worker只能够加载js文件,不能加载ts文件
    • postMessage的参数是 1 data(可以被结构化克隆的) ,2 使用transfer的数据

md5-single.worker?wo…file&type=classic:1 () Uncaught

  1. SyntaxError: Unexpected token '<' (at md5-single.worker?wo…le&type=classic:1:1)

  • 没有加载正确的worker文件