likes
comments
collection
share

Web Worker(遥遥领先的速度)+大文件分片

作者站长头像
站长
· 阅读数 11

大文件分片

大家好, 我今天是华为冲锋战神遥遥领先瑜, 在网上看了大半天mate60 现在耳朵里都是遥遥领先, 所以一鼓作气给大家分享一篇大文件分片+webWorker的文章, 通过写作这篇文章, 自己也学习到了不少知识, 比如通过js获取电脑cpu线程, 那么话不多说直接开始

1. 初始化, 搭设架子

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>大文件分片</title>
  <style></style>
</head>

<body>
  <input type="file" id="fileRef" />
  <script type="module" src="./main.js">


  </script>
</body>

</html>

2. 读取单个文件的分片, 并进行加密

这里使用的是SparkMD5进行加密, 可自行下载并导入

import { createChunks } from './createChunks'
// 规定每次切片的文件大小
const CHUNK_SIZE = 1024 * 1024 * 5 // 5MB

export async function cutFile (file) {
  // 生成每一个切片, 分片是耗时的所以是异步操作
  const chunk = await createChunks(file, 1, CHUNK_SIZE)
  // 等待分片完成, 就可以拿到这一个分片的信息
  console.log(chunk)
}

定义cutFile函数来进行文件切片, 并且返回该切片的开始结束, 第几个和hash值

Web Worker(遥遥领先的速度)+大文件分片

import SparkMD5 from "./md5.js";
/**
 * @param {*} file 文件
 * @param {*} index 第几个分片
 * @param {*} chunkSize 每一个分片的大小
 */
export function createChunks (file, index, chunkSize) {
  return new Promise((resolve, reject) => {
    // 开始第几个*分片的大小
    const start = index * chunkSize
    //   结束时start + 分片的大小
    const end = start + chunkSize
    const fileReader = new FileReader()
       spark.append(e.target.result);
      const files = file.slice(start, end);
    // 读取文件的分片 读取完成后触发onload事件
    fileReader.onload = e => {
      console.log(e)
      const spark = Md5(e.target.result)
      resolve({
        start,
        end,
        index,
        hash: spark.end(),
        files,
      })
    }
    // 读取文件的分片
    fileReader.readAsArrayBuffer(file.slice(start, end))
  })
}

现在我们只读到了一个分片, 但是我们需要读取所有的分片, 所以需要计算目前这个文件需要分成多少个分片, 然后一个一个去读

所以可以利用文件的总大小 / 自定义定义文件切片的大小 就可以得到总切片数量

注意这里需要向上去整, 出现任何的小数点都需要包含一片内

export async function cutFile (file) {
  // 计算文件的切片数量
  const chunks = Math.ceil(file.size / CHUNK_SIZE)
  console.log(chunks, 'chunks')
  // 生成每一个切片, 分片是耗时的所以是异步操作
  const chunk = await createChunks(file, 1, CHUNK_SIZE)
  // 等待分片完成, 就可以拿到这一个分片的信息
  console.log(chunk)
}

3. 将文件进行全部切片

已经拿到了文件切片的总数量, 这里就可以循环, 并存储到一个数组中即可

export async function cutFile (file) {
  const result = []
  // 计算文件的切片数量
  const chunks = Math.ceil(file.size / CHUNK_SIZE)
  // 生成每一个切片, 分片是耗时的所以是异步操作
  for (let i = 0; i < chunks; i++) {
    const chunk = await createChunks(file, i, CHUNK_SIZE)
    // 等待分片完成, 就可以拿到这一个分片的信息
    result.push(chunk)
  }
  return result
}

此时我们根据入口函数来打印看一下结果

Web Worker(遥遥领先的速度)+大文件分片

文件被切割成了103分并且都保存在了一个数组中, 消耗了2.3秒

4. 分析如何对分片进行优化

这里我上传的文件是500m大小, 但是我上传是好几个g 一定会更加的延迟, 造成线程长时间的阻塞, 原因是这里使用到了MD5, 进行了大量的计算

所以如何避免线程的阻塞就是优化的关键, 在js中有Web Worker

mdn的概念说: 使得在一个独立于 Web 应用程序主执行线程的后台线程中运行脚本操作成为可能。这样做的好处是可以在独立线程中执行费时的处理任务,使主线程(通常是 UI 线程)的运行不会被阻塞/放慢。

5. 使用webWoker进行优化

1. 搭设架子

首先需要定义开始线程的数量, 并且按照数量去开启新的线程, 并搭设架子

至于要向worker发送什么消息, 以及接收返回的值, 我们稍后分析

// 定义线程数量
const THREAD_COUNT = 4 // 4个线程

export async function cutFile (file) {
  const result = []
  // 计算文件的切片数量
  const chunks = Math.ceil(file.size / CHUNK_SIZE)
  // 生成每一个切片, 分片是耗时的所以是异步操作
  //   for (let i = 0; i < chunks; i++) {
  //     const chunk = await createChunks(file, 1, CHUNK_SIZE)
  //     // 等待分片完成, 就可以拿到这一个分片的信息
  //     result.push(chunk)
  //   }
  // 创建新的线程
  for (let i = 0; i < THREAD_COUNT; i++) {
    const worker = new Worker('./worker.js', { type: 'module' })
    worker.postMessage(???)// 向 worker 线程发送消息
    worker.onmessage = e => {
        // 接收到 worker 线程返回的消息
        console.log(e)
    }
  }
  return result
}

2. 计算每一个线程需要处理的切片数量

  1. 首先必须是文件file, 需要处理的文件
  2. 其次是文件的每一个尺寸, 就是定义好的CHUNK_SIZE = 1024 * 1024 * 5 // 5MB
  3. 要处理的分片区间(也就是开始下标和结束的下标) 例如 103个文件 , 这里一共开启了4个线程, 那么就需要这四个线程分别去处理103个分片文件

以上三个条件已知的是file文件以及每一个文件的chunksize, 只有第三个是不知道的, 也就是不知道开始和结束值的分片期间

首先需要计算每一个线程需要处理的切片数量

const workerChunkCount = Math.ceil(切片数量 / 定义线程数量)

开启线程, 并计算每个线程的开始索引和结束索引, 这里需要在遍历中通过i下标进行计算

// 这里的worker稍后解释如何定义, 注意这里要写type: module 因为引入了其他模块需要使用
const worker = new Worker('./worker.js', { type: 'module' })   
 // 计算每个线程的开始索引和结束索引
    const startIndex = i * workerChunkCount
    let endIndex = startIndex + workerChunkCount
    // 防止最后一个线程结束索引大于文件的切片数量的总数量
    if (endIndex > chunks) {
      endIndex = chunks
    }

最后进行发送

    // 向 worker 线程发送消息
    worker.postMessage({
      file, // 文件
      CHUNK_SIZE, // 文件大小
      startIndex, // 开始下标
      endIndex // 结束下标
    })

完整代码

// 规定每次切片的文件大小
const CHUNK_SIZE = 1024 * 1024 * 5 // 5MB
// 定义线程数量
const THREAD_COUNT = 4 // 4个线程

export async function cutFile (file) {
  const result = []
  // 计算文件的切片数量
  const chunks = Math.ceil(file.size / CHUNK_SIZE)
  // 计算每一个线程需要处理的切片数量
  const workerChunkCount = Math.ceil(chunks / THREAD_COUNT)
  // 生成每一个切片, 分片是耗时的所以是异步操作
  //   for (let i = 0; i < chunks; i++) {
  //     const chunk = await createChunks(file, 1, CHUNK_SIZE)
  //     // 等待分片完成, 就可以拿到这一个分片的信息
  //     result.push(chunk)
  //   }
  // 创建新的线程
  for (let i = 0; i < THREAD_COUNT; i++) {
    const worker = new Worker('./worker.js', { type: 'module' })
    // 计算每个线程的开始索引和结束索引
    const startIndex = i * workerChunkCount
    let endIndex = startIndex + workerChunkCount
    // 防止最后一个线程结束索引大于文件的切片数量的总数量
    if (endIndex > chunks) {
      endIndex = chunks
    }
     // 向 worker 线程发送消息
    worker.postMessage({
      file, // 文件
      CHUNK_SIZE, // 文件大小
      startIndex, // 开始下标
      endIndex // 结束下标
    })
 
    worker.onmessage = e => {
      // 接收到 worker 线程返回的消息
      console.log(e)
    }
  }
  return result
}

3. 接收到 worker 线程返回的消息

  • 接收worker返回的信息, 我们需要将信息按照顺序添加到result数组中,
  • 并且每次接收到一个就需要关闭掉一个线程
  • 如果线程全部完成了, 需要将result数组进行抛出
let finishCount = 0 // 记录线程开启的次数
worker.onmessage = (e) => {
        // 接收到 worker 线程返回的消息
        for (let i = startIndex; i < endIndex; i++) {
          result[i] = e.data[i - startIndex];
        }
        worker.terminate();
        finishCount++;
      // 如果记录的开启线程 = 定义的开启线程次数
        if (finishCount === THREAD_COUNT) {
          // 通知主线程, 并返回结果
         resolve(result);
        }
   };

这里使用resolve 因为需要等待, 所以需要是异步操作

完整代码

export const cutFile = async (file) => {
  return new Promise((resolve, reject) => {
    // 规定每次切片的文件大小
    const CHUNK_SIZE = 1024 * 1024 * 5; // 5MBå
    // 定义线程数量
    const THREAD_COUNT = 4; // 4个线程
    let result = [];
    // 计算文件的切片数量
    const chunks = Math.ceil(file.size / CHUNK_SIZE);
    // 计算每一个线程需要处理的切片数量
    const workerChunkCount = Math.ceil(chunks / THREAD_COUNT);
    let finishCount = 0; // 完成的线程数量

    // 生成每一个切片, 分片是耗时的所以是异步操作
    //   for (let i = 0; i < chunks; i++) {
    //     const chunk = await createChunks(file, i, CHUNK_SIZE);
    //     // 等待分片完成, 就可以拿到这一个分片的信息
    //     result.push(chunk);
    //   }
    //   return result;
    // 创建新的线程
    for (let i = 0; i < THREAD_COUNT; i++) {
      const worker = new Worker("./worker.js", {
        type: "module",
      });
      //   const worker = new Worker(worderPath)
      // 计算每个线程的开始索引和结束索引
      const startIndex = i * workerChunkCount;
      let endIndex = startIndex + workerChunkCount;
      // 防止最后一个线程结束索引大于文件的切片数量的总数量
      if (endIndex > chunks) {
        endIndex = chunks;
      }
      worker.postMessage({
        file,
        CHUNK_SIZE,
        startIndex,
        endIndex,
      });

      worker.onmessage = (e) => {
        // 接收到 worker 线程返回的消息
        for (let i = startIndex; i < endIndex; i++) {
          result[i] = e.data[i - startIndex];
        }
        worker.terminate();
        finishCount++;
        if (finishCount === THREAD_COUNT) {
          // 所有线程都完成了
          // 通知主线程
          //   console.log(result);
          resolve(result);
        }
      };
    }
  });
};

4. worker文件

因为这里开启了worker线程, 所以需要执行./worker的逻辑

 const worker = new Worker("./worker.js", {
        type: "module",
 });

很显然 这里就是将file,CHUNK_SIZE,startIndex,endIndex 这几个有用的数据拿出来, 并且执行读取单个分片的方法 createChunks 这里之前已经定义好了, 所以直接调用

// 之前 添加worker的信息
worker.postMessage({
        file,
        CHUNK_SIZE,
        startIndex,
        endIndex,
  });
  • 那么, 需要调用几次createChunks 方法呢? 可以通过for循环startIndex,endIndex 来操作
  • 并且createChunks 中读取文件是需要异步操作的, 我们希望同时全部读取完毕后在返回, 不然就是上一个读取完, 才会读取下一个, 浪费了时间

按照以上这两点思路, 就可以写逻辑了

import { createChunks } from "./createChunks.js";

onmessage = async (e) => {
  const arr = [];
  const { file, CHUNK_SIZE, startIndex, endIndex } = e.data;
  // console.log(file, CHUNK_SIZE, startIndex, endIndex);
  for (let i = startIndex; i < endIndex; i++) {
    arr.push(createChunks(file, i, CHUNK_SIZE));
  }
  //  Promise.all=> 同时进行异步操作
  const chunks = await Promise.all(arr);
  // 提交线程信息
  postMessage(chunks);
};

看一下结果吧

Web Worker(遥遥领先的速度)+大文件分片

很明显, 从之前的2秒多 多现在的0.2米, 速度快了10倍.

6. Js 获取电脑cpu的线程

突发奇想, js能不能获取电脑cpu的最大线程数量呢, 这样就可以每次追求最快的速度, 现在已经是晚上1点多了, 偷懒直接问了chat, 哈哈 搜了一下还真有

Web Worker(遥遥领先的速度)+大文件分片

这里果断再优化一下, 最求卓越

    // 获取核心线程的数量
    const THREAD_COUNT = navigator.hardwareConcurrency || 4; 
    console.log(navigator.hardwareConcurrency);

Web Worker(遥遥领先的速度)+大文件分片

果然又快了一倍多, 美滋滋~手动撒花~~✿✿ヽ(°▽°)ノ✿

这里附上git链接, 有需要的可以直接下载源码查看

彩蛋

最近华为mate60的突然发布, 是让很多人包括我在内都欣喜不已!华为伴着遥遥领先和麒麟芯片再次回来了! 近年来,华为面临了来自美国政府的技术制裁,但这并没有阻止华为继续前行,如同一叶轻舟,穿越千山万水,创造着无尽可能. 有网友拍摄深夜的华为总部, 总是灯火通明, 里面的人都是顶级的优秀人才. 不仅优秀更是勤劳付出.

年底争取给 给自己换一台华为手机. #华为 #麒麟芯片 #科技强国 🚀🇨🇳