JS利用Worker多线程大文件切片上传

news2024/12/24 3:13:39

在做前端上传时,会遇到上传大文件,大文件就要进行分片上传,我们整理下思路,实现一个分片上传,最终我们要拿到每一个分片的hash值,index 分片索引,以及分片blob,如下:
在这里插入图片描述

一、实现切片

index.html: 我们先创建一个html文件,用于处理选择文件,进而分片,这里利用spark-md5获取文件hash值

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>文件分片</title>
</head>
<body>
<input type="file" id="input"></input>
<script type="module">
  import { createChunk } from './main.js'

  document.getElementById('input').onchange = event => {
    let files = event.target.files
    if (files.length) {
      // 进行分片
      createChunk(files[0])
    }
  }
</script>
</body>
</html>

main.js 默认一个分片5M,这里要向上取整,计算出分片数量

import SparkMD5 from 'https://esm.sh/spark-md5@3.0.2'

const CHUNK_SIZE = 1024 * 1024 * 5 // 分片大小5M

/**
 * @description 进行分片
 * @param {Object} file: 当前要处理的任务对象
 */
function createChunk(file) {
  // 计算分片数量
  let count = Math.ceil(file.size / CHUNK_SIZE)
  console.log(count)
}

在这里插入图片描述
然后循环处理

/**
 * @description 进行分片
 * @param {Object} file: 当前要处理的任务对象
 */
function createChunk(file) {
  // 计算分片数量
  let count = Math.ceil(file.size / CHUNK_SIZE)
  for (let i = 0; i < count; i++) {
    // 分片
    splitChunk(file, i, CHUNK_SIZE)
  }
}

对文件进行切片,利用file.slice(start, end)对文件进行切片处理

/**
 * @description 切片
 * @param {Object} file: file对象
 * @param {Number} i: 当前处理的第几个任务
 * @param {Number} size: 一个切片的大小
 */
function splitChunk(file, i, size) {
   let start = i * size
   let end = (i + 1) * size
   let blob = file.slice(start, end)
   console.log(blob)
}

可以看到,切为若干份
在这里插入图片描述

然后获取切片的hash值,并且记录切片的起始/结束位置和切片索引index,最终返回promise

/**
 * @description 切片
 * @param {Object} file: file对象
 * @param {Number} i: 当前处理的第几个任务
 * @param {Number} size: 一个切片的大小
 */
function splitChunk(file, i, size) {
  return new Promise((resolve, reject) => {
    let start = i * size
    let end = (i + 1) * size
    let blob = file.slice(start, end)
    // 获取文件hash值
    getFileHash(blob)
      .then(res => {
        resolve({
          blob: blob,
          start,
          end,
          index: i,
          hash: res
        })
      })
  })
}

/**
 * @description 获取文件hash值
 * @param {Object} file: 源文件信息
 */
function getFileHash(file) {
  let spark = new SparkMD5.ArrayBuffer()
  return new Promise(function (resolve, reject) {
    let fileReader = new FileReader()

    fileReader.onload = function (e) {
      let buffer = e.target.result
      if (file.size != buffer.byteLength) {
        reject("获取文件hash失败,按理说不可能");
      } else {
        spark.append(buffer) // 解析 Buffer
        resolve(spark.end())
      }
    };

    fileReader.onerror = function () {
      reject("文件初始化读取Buffer失败");
    };

    fileReader.readAsArrayBuffer(file);
  });
}

然后对返回的结果进行批处理,这里我们可以打印下处理的时间

/**
 * @description 进行分片
 * @param {Object} file: 当前要处理的任务对象
 */
function createChunk(file) {
  let promises = []
  console.time('splitChunk')
  // 计算分片数量
  let count = Math.ceil(file.size / CHUNK_SIZE)
  for (let i = 0; i < count; i++) {
    // 分片
    promises.push(splitChunk(file, i, CHUNK_SIZE))
  }
  Promise.all(promises)
    .then(res => {
      console.log(res)
      console.timeEnd('splitChunk')
    })
}

可以看到,处理了4秒左右,主要是花费在处理文件hash上,获取文件hash为同步任务,且占用主线程,这对于单线程的js来讲,如果文件过大,会影响当前用户的使用体验
在这里插入图片描述

二、多线程优化

上面我们已经实现切片,但是如果文件过大,带来的影响较大,所以我们可以利用js Worker进行多线程优化,这里我们判断客户端的cpu内核数量,进而开启对应数量的线程

利用浏览器navigator.hardwareConcurrency获取当前cpu内核数量,可以看到,本机的内核数为8
在这里插入图片描述
这里处理线程的开启数量

import SparkMD5 from 'https://esm.sh/spark-md5@3.0.2'

const CHUNK_SIZE = 1024 * 1024 * 5 // 分片大小5M
const KERNEL_COUNT = navigator.hardwareConcurrency || 4 // 内核数量,如果取不到则为4

/**
 * @description 进行分片
 * @param {Object} file: 当前要处理的任务对象
 */
function createChunk(file) {
  // 计算分片数量
  let count = Math.ceil(file.size / CHUNK_SIZE)
  // 计算线程开启数量
  let workerCount = Math.ceil(count / KERNEL_COUNT)
    // 计算线程开启数量
  let workerCount = Math.ceil(count / KERNEL_COUNT)
  for (let i = 0; i < workerCount; i++) {
    // 创建一个线程,并且分配任务,这里要指定为module模块化
    let worker = new Worker('./worker.js', { type: "module" })
    // 因为线程数量是向上取整,有除不尽的情况,这里要处理下结束的chunkIndex,如果最后一个chunk大于总chunk数,则写死
    let end = (i + 1) * KERNEL_COUNT
    if (end > count) {
      end = count
    }
    // 分配任务
    worker.postMessage({
      file,
      CHUNK_SIZE,
      startChunkIndex: i * KERNEL_COUNT,
      endChunkIndex: end
    })
    // 接收处理结果
    worker.onmessage = e => {
    
    }
  }
}

多线程查看接收到的任务,一共开启8个线程,进行切片
worker.js:线程worker

// 处理线程收到消息
onmessage = e => {
  let {
    file,
    CHUNK_SIZE,
    startChunkIndex,
    endChunkIndex
  } = e.data
  console.log(file, CHUNK_SIZE, startChunkIndex, endChunkIndex)
}

在这里插入图片描述
worker.js:接下来,我们进行切片,然后返回切片结果

import { splitChunk } from './main'

// 处理线程收到消息
onmessage = async e => {
  let {
    file,
    CHUNK_SIZE,
    startChunkIndex,
    endChunkIndex
  } = e.data
  let promises = []
  for (let i = startChunkIndex; i < endChunkIndex; i++) {
    promises.push(splitChunk(file, i, CHUNK_SIZE))
  }
  let chunks = await Promise.all(promises)
  console.log(chunks)
  postMessage(chunks)
}

打印下,可以看到8个线程各自处理切片,一共57个切片,最后一个线程只有一个切片任务, 7 * 8 + 1 = 57
在这里插入图片描述
main.js:然后我们接收处理结果,然后保存起来,因为这里的线程谁先完事儿是未知数,所以需要特殊处理下:

/**
 * @description 进行分片
 * @param {Object} file: 当前要处理的任务对象
 */
export function createChunk(file) {
  return new Promise(((resolve, reject) => {
    let promises = []
    // 结果
    let result = []
    // 计算分片数量
    let count = Math.ceil(file.size / CHUNK_SIZE)
    // 计算线程开启数量
    let workerCount = Math.ceil(count / KERNEL_COUNT)
    // 当前线程执行完毕的数量
    let finishCount = 0
    for (let i = 0; i < workerCount; i++) {
      // 创建一个线程,并且分配任务
      let worker = new Worker('./worker.js', { type: "module" })
      // 开始
      let start = i * KERNEL_COUNT
      // 因为线程数量是向上取整,有除不尽的情况,这里要处理下结束的chunkIndex,如果最后一个chunk大于总chunk数,则写死
      let end = (i + 1) * KERNEL_COUNT
      if (end > count) {
        end = count
      }
      // 分配任务
      worker.postMessage({
        file,
        CHUNK_SIZE,
        startChunkIndex: start,
        endChunkIndex: end
      })
      // 接收处理结果
      worker.onmessage = e => {
      	// 这里为了避免顺序乱,取当前的执行索引
        for (let i = start; i < end; i++) {
          result[i] = e.data[i - start]
        }
        worker.terminate() // 结束任务
        finishCount ++ // 完成数量++
        if (finishCount === workerCount) {
          resolve(result)
        }
      }
    }
  }))
}

index.html:我们可以打印下结果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>文件分片</title>
</head>
<body>
<input type="file" id="input"></input>
<script type="module">
  import { createChunk } from './main.js'

  document.getElementById('input').onchange = async event => {
    let files = event.target.files
    if (files.length) {
      console.time('splitChunk')
      // 进行分片
      let res = await createChunk(files[0])
      console.log(res)
      console.timeEnd('splitChunk')
    }
  }
</script>
</body>
</html>

可以看到,效率极大提升,由4秒提升到1秒,最主要不影响用户体验
在这里插入图片描述
在这里插入图片描述
断点续传就更简单了,服务端记录任务进度即可,这里就不说了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1489405.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

递归学习资料

思路 例题 package 递归;public class 反向打印字符串 {public static void main(String[] args) {f("ABC",0);}static void f(String str,int n){if (nstr.length()){return;}f(str,n1);System.out.println(str.charAt(n)"");} }多路递归 递归优化 -剪枝…

(vue)复合型输入框el-input输入数字类型,e,+,-等特殊符号可以输入

(vue)复合型输入框el-input输入数字类型&#xff0c;e&#xff0c;&#xff0c;-等特殊符号可以输入 效果 代码 <el-form-item label"分数区间"><el-inputplaceholder"请输入内容"v-model.number"formInline.scoreIntervalValue"clas…

Oracle 11g升级19c 后部分查询功能很慢

*Oracle 11g升级19c 后部分查询功能很慢 今天生产突然有个查询非常慢&#xff0c;日志显示执行了50秒左右&#xff0c;但是从日志中拿出SQL在PLSQL执行&#xff0c;发现用时不到1秒&#xff0c;查看SQL,怀疑是下面几种原因导致 1、使用函数不当 UNIT.UNIT_CODE LIKE CONCAT(‘…

2核4G云服务器租用价格_2核4G云主机优惠价格_2024年报价

租用2核4G服务器费用价格&#xff0c;2核4G云服务器多少钱一年&#xff1f;1个月费用多少&#xff1f;阿里云2核4G服务器30元3个月、轻量应用服务器2核4G4M带宽165元一年、企业用户2核4G5M带宽199元一年&#xff1b;腾讯云轻量2核4G服务器5M带宽165元一年、252元15个月、540元三…

SEACells从单细胞基因组学数据推断转录和表观基因组细胞状态

摘要 Metacells是从单细胞测序数据中衍生出的细胞分组&#xff0c;代表了高度粒度化、独特的细胞状态。在这里&#xff0c;我们提出了单细胞细胞状态聚合&#xff08;SEACells&#xff09;&#xff0c;这是一种用于识别Metacells的算法&#xff0c;它克服了单细胞数据稀疏性的…

leetcode 2.27

leetcode hot 100 哈希1.字母异位词分组2.最长连续序列 双指针1.盛最多水的容器2.和为 K 的子数组 数组1.除自身以外数组的乘积 哈希 1.字母异位词分组 49. 字母异位词分组 方法一&#xff1a;排序 由于互为字母异位词的两个字符串包含的字母相同&#xff0c;因此对两个字符…

C++设计模式之——桥接模式详解和代码实例

文章目录 桥接模式详解&#xff1a;C代码实例进一步阐述桥接模式的优点和适用场景桥接模式的实际应用场景还包括但不限于以下几种情况&#xff1a; 桥接模式(Bridge Pattern)是一种结构型设计模式&#xff0c;它将抽象部分与其实现部分分离&#xff0c;使它们都可以独立变化。这…

Arduino与processing之间的通信——进阶版

本次需要实现Arduino获取板子的偏转角度并通过串口发送给processing&#xff0c;processing部分根据传输过来的各个轴的偏转角度建立对应偏转角度的3D模型。 这就涉及了两个轴正负方向的偏转&#xff0c;我的实现思路是使用串口传输 字母数字 格式的信息&#xff0c;字母用来判…

Corel 会声会影 2023 激活码 会声会影 2023 序列号生成器

会声会影 2023 已经出来很长时间了&#xff0c;但是对它的热爱一直持续不减&#xff0c;今天我给大家带来2023版本为用户带来的多个全新功能&#xff0c;可以更好的编辑视频&#xff0c;不过软件还是付费的&#xff0c;为此我带来了会声会影 2023序列号生成器&#xff0c;可以轻…

算法沉淀——动态规划之01背包问题(leetcode真题剖析)

算法沉淀——动态规划之01背包问题 01.【模板】01背包02.分割等和子集03.目标和04.最后一块石头的重量 II 01背包问题是一类经典的动态规划问题&#xff0c;通常描述为&#xff1a;有一个固定容量的背包&#xff0c;以及一组物品&#xff0c;每件物品都有重量和价值&#xff0c…

【蓝桥杯】分巧克力

一.题目描述 二.输入描述 三.输出描述 四.问题分析 //分巧克力 #include <iostream> #include <algorithm>using namespace std;const int N1e510; int n,k,h[N],w[N];bool judge(int mid){int cnt0;for(int i0;i<n;i){cnt(h[i]/mid)*(w[i]/mid);if(cnt>k)r…

【短时交通流量预测】基于Elman神经网络

课题名称&#xff1a;基于Elman神经网络的短时交通流量预测 版本时间&#xff1a;2023-04-27 代码获取方式&#xff1a;QQ&#xff1a;491052175 或者 私聊博主获取 模型简介&#xff1a; 城市交通路网中交通路段上某时刻的交通流量与本路段前几个时段的交通流量有关&#…

数仓项目6.0(一)

尚硅谷大数据项目【电商数仓6.0】企业数据仓库项目_bilibili 数据流转过程 用户➡️业务服务器➡️数据库存储➡️数仓统计分析➡️数据可视化 数据仓库处理流程&#xff1a;数据源➡️加工数据➡️统计筛选数据➡️分析数据 数据库不是为了数据仓库服务的&#xff0c;需要…

【牛客】VL63 并串转换

题目 描述 题目描述&#xff1a; 设计一个模块进行并串转换&#xff0c;要求每四位d输为转到一位dout输出&#xff0c;输出valid_in表示此时的输入有效 信号示意图&#xff1a; clk为时钟 rst为低电平复位 valid_in 表示输入有效 d 信号输入 dout 信号输出 波形示意图&…

TypeError: the JSON object must be str, bytes or bytearray, not dict

参考文章&#xff1a;https://blog.csdn.net/yuan2019035055/article/details/124934362 Python基础系列&#xff08;一&#xff09;搞懂json数据解析与字典之间的关系 代码&#xff1a; 报错信息: TypeError: the JSON object must be str, bytes or bytearray, not dict …

光伏并网逆变器低电压穿越控制Simulink模型!

适用平台&#xff1a;MatlabSimulink 简介 当电网突发跌落故障时&#xff0c;电网电压降低会使并网电流增大、母线电压升高。当跌落程度较轻时&#xff0c;并网电流仍在逆变器安全运行的范围内&#xff1b;但跌落程度较深时&#xff0c;会引发逆变器过流和母线过压等问题&…

CommandLineRunner的使用

背景 在项目启动时需要做一些数据预加载或者某些操作&#xff0c;需要怎么办呢&#xff0c;方法其实有好几种&#xff0c;这里主要讲一下SpringBoot提供的CommandLineRunner接口的使用。一、案例说明以及实现 1.实现CommandLineRunner接口 定义一个类实现CommandLineRunner接…

在 Linux 上用 zram 替代传统交换空间 | Linux 中国

我在我的电脑上花了很多时间&#xff08;我是说工作&#xff09;&#xff0c;我发现了很多有趣的东西。其中最近引起我注意的是 zram0 设备。我是在几个月前写一篇文章时第一次注意到它&#xff0c;它显示在 lsblk 命令的输出中&#xff1a; # lsblk NAME MAJ:MIN RM…

Mybatis-Plus介绍

目录 一、Mybatis-Plus简介 1.1、介绍 1.2、特性 1.3、架构 1.4、Mybatis-Plus与Mybatis的区别 二、快速入门 2.1、首先创建数据库mybatis-plus 2.2、创建user表 2.3、插入数据 2.4、创建Spring-Boot项目 2.5、添加依赖 2.6、连接数据库 一、Mybatis-Plus简介 1.1、…

内网穿透的应用-如何修改Nginx服务location代理转发规则结合cpolar实现无公网ip环境访问内网站点

文章目录 1. 下载windows版Nginx2. 配置Nginx3. 测试局域网访问4. cpolar内网穿透5. 测试公网访问6. 配置固定二级子域名7. 测试访问公网固定二级子域名 1. 下载windows版Nginx 进入官方网站(http://nginx.org/en/download.html)下载windows版的nginx 下载好后解压进入nginx目…