JavaScript 使用 Web Worker 实现大文件分片上传与断点续传
本文介绍如何利用 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 使用多线程实现大文件上传的方法,更多相关内容可搜索本站历史文章或浏览后续文章,感谢支持。
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
相关标签:
