JavaScript 使用 Web Worker 实现大文件分片上传与断点续传

作者:半夏时间:2026-10-31 16:45:19浏览:480

本文介绍如何利用 JavaScript 的 Web Worker 多线程机制解决大文件上传时的页面卡顿问题。通过将文件分片计算 Hash 值等耗时操作移至后台线程,实现主线程与计算线程分离,从而支持断点续传和重复文件检测,确保浏览器响应流畅。

开发者要求 JavaScript 处理一个十 GB 级别的文件,具体任务包括将文件分片,并为每个分片计算 Hash 值,以便服务端通过 Hash 值判断分片是否已上传(实现断点续传),同时计算整个文件的 Hash 值以识别重复文件,避免重复上传。

JavaScript 回应称,若由主线程直接执行此类复杂计算,会导致浏览器界面卡死。

const CHUNK_SIZE = 5 * 1024 * 1024; // 分片大小
async function getFile(file) {
  const result = [];
  const chunkLength = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < chunkLength; i++) {
    const chunk = await getChunk(file, CHUNK_SIZE, i);
    result.push(chunk);
  }
}

function getChunk(file, size, index) {
  return new Promise((resolve, reject) => {
    const start = index * size;
    const end = start + size;
    const chunkFile = file.slice(start, end);
    const fr = new FileReader();
    fr.onload = function(e) {
      const arrBuffer = e.target.result;
      const hash = SparkMD5.ArrayBuffer.hash(arrBuffer);
      resolve({
        start,
        end,
        chunkFile,
        index,
        hash
      })
    }
    fr.readAsArrayBuffer(chunkFile);
  })
}

开发者对此表示惊讶,质疑 JavaScript 是否真的会因此停止响应。

探讨 JavaScript 为何会在此场景下停止响应。

在浏览器的事件循环机制中,存在多个线程处理不同任务,包括默认的 HTTP 线程、IO 线程等。

若在浏览器中进行复杂计算且全部在主线程执行,会导致主线程阻塞,进而引发页面响应延迟和界面卡顿。

探讨是否有方法实现主线程与计算线程的分离。

解决方案是使用 Web Worker。

Web Worker 允许开启独立线程处理复杂计算任务,计算完成后通过回调通知主线程,主线程仅负责处理获取结果后的后续逻辑。

接下来学习 Web Worker 的具体使用方法。

第一步:创建 Worker 实例。

const worker = new Worker("./worker.js");
// 如果需要指定worker的js可以使用ESM,可以添加type参数
const worker = new Worker("./worker.js", { type: "module" });

第二步:通知 Worker 开始执行任务。

此处的 Worker 代码引用自刚创建的 worker.js 文件。

worker.postMessage("开始工作");

第三步:监听 Worker 发送的消息。

worker.onmessage = (e) => {
  // 内部worker执行完了,或者执行到某个节点了
};

第四步:关闭 Worker 实例。

worker.terminate();

第五步:了解 Worker 内部如何与主线程进行通信。

self.onmessage = (e) => {
  // 收到了外部worker的消息
  // 复杂逻辑
  self.postMessage("计算完成");
};

利用 Worker 替代主线程执行复杂计算任务。

const CHUNK_SIZE = 5 * 1024 * 1024; // 分片大小
const worker = new Worker('./worker.js', {
  type: 'module'
});

fileDom.onchange = function(e) {
  const file = e.target.files[0];
  worker.postMessage([file, CHUNK_SIZE]);
}
// worker.js
self.onmessage = async (e) => {
  const [file, CHUNK_SIZE] = e.data;
  const result = [];
  const chunkLength = Math.ceil(file.size / CHUNK_SIZE);
  for (let i = 0; i < chunkLength; i++) {
    const chunk = await getChunk(file, CHUNK_SIZE, i);
    result.push(chunk);
  }
  // 处理完成了
  self.postMessage(result);
}

function getChunk(file, size, index) {
  return new Promise((resolve, reject) => {
    const start = index * size;
    const end = start + size;
    const chunkFile = file.slice(start, end);
    const fr = new FileReader();
    fr.onload = function(e) {
      const arrBuffer = e.target.result;
      const hash = SparkMD5.ArrayBuffer.hash(arrBuffer);
      resolve({
        start,
        end,
        chunkFile,
        index,
        hash
      })
    }
    fr.readAsArrayBuffer(chunkFile);
  })
}

多线程机制允许开启多个 Worker,例如同时开启多个线程以加速处理。

// 直接开启四个worker
const MAX_WORKER_NUM = 4;
const workers = new Array(MAX_WORKER_NUM).fill(0).map(() => new Worker('./worker.js', { type: 'module' }));
const wholeFileWorker = new Worker('./hashWholeFile.js', { type: 'module' });
let finishedCount = 0;
fileDom.onchange = function(e) {
  const file = e.target.files[0];
  // 计算一下一共有多少个分片
  const chunkLength = Math.ceil(file.size / CHUNK_SIZE);
  // 每一个worker要完成多少分片
  // 假如有99个分片,那第一个worker要处理1-25,第二个26-50,第三个51-75,第四个76-99
  // 我们是程序员,所以每一个index都要-1
  const workerSize = Math.ceil(chunkLength / MAX_WORKER_NUM);
  for(let i = 0; i < MAX_WORKER_NUM; i++) {
    const worker = workers[i];
    // 帮worker计算好分片任务的起始位置和结束位置
    const startIndex = i * workerSize;
    const endIndex = Math.min(start + workerSize, chunkLength);
    worker.postMessage([file, CHUNK_SIZE, startIndex, endIndex]);
    worker.onmessage = (e) => {
      finishedCount++;
      worker.terminate();
      // 计算完一部分的hash就可以开始上传了,每一个返回结果里面有index,可以告诉后端传递的是哪个分片,信息已经足够了
      
    }
  }
  // 前面先计算分片的hash,有分片计算好的hash就可以直接开始上传了
  wholeFileWorker.postMessage([file]);
  wholeFileWorker.onmessage = (e) => {
    // 最后处理整个文件的hash
    // 这样整体效果就是,用户选择文件之后,可以快速的开始上传进度条,
    // 如果是之前上传了部分,并且开始上传的分片之前已经上传好了,那么可以快速跳过这些分片,直接上传剩下的分片
    // 如果之前整个文件都上传了,那么进度条会从很少的地方直接跳到100%
  }

}
// worker.js
import "./md5.min.js"
self.onmessage = async (e) => {
  const [file, CHUNK_SIZE, startIndex, endIndex] = e.data;
  const result = [];
  for (let i = startIndex; i < endIndex; i++) {
    const chunk = await getChunk(file, CHUNK_SIZE, i);
    result.push(chunk);
  }
  // 处理完成了
  self.postMessage(result);
}

function getChunk(file, size, index) {
  return new Promise((resolve, reject) => {
    const start = index * size;
    const end = start + size;
    const chunkFile = file.slice(start, end);
    const fr = new FileReader();
    fr.onload = function(e) {
      const arrBuffer = e.target.result;
      const hash = SparkMD5.ArrayBuffer.hash(arrBuffer);
      resolve({
        start,
        end,
        chunkFile,
        index,
        hash
      })
    }
    fr.readAsArrayBuffer(chunkFile);
  })
}

// hashWholeFile.js
import "./md5.min.js"
self.onmessage = (e) => {
  const [file] = e.data;
  const hash = SparkMD5.ArrayBuffer.hash(file);
  self.postMessage(hash);
}

本文介绍了 JavaScript 使用多线程实现大文件上传的方法,更多相关内容可搜索本站历史文章或浏览后续文章,感谢支持。

作者最新文章