一、方案概述
大文件分片上传方案旨在解决大文件上传过程中的性能问题、网络中断问题以及用户体验问题。通过将大文件分割成多个小分片,分别上传后再在服务器端合并,可以有效提高上传效率和成功率。
二、具体实现步骤
(一)文件选择与初步处理
-
HTML 文件选择 :使用
<input type="file">
元素,让用户选择本地要上传的大文件。 -
获取文件对象 :通过 JavaScript 监听文件选择事件,获取用户选择的文件对象。
<input type="file" id="fileInput">
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
// 后续分片上传操作
}
});
(二)文件分片
-
定义分片大小 :通常设置为 5MB 左右,可以根据实际需求调整。
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
-
计算分片数量 :根据文件大小和分片大小计算总分片数。
const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
-
分片处理 :将文件按分片大小分割成多个小块。
for (let i = 0; i < chunkCount; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
// 上传分片
}
(三)分片上传
-
并发上传控制 :获取设备的线程数量,合理控制并发上传的线程数。
const THREAD_COUNT = navigator.hardwareConcurrency || 4;
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT);
-
多线程上传实现 :使用 Web Workers 实现多线程上传,提高上传效率。
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker('./worker.js', {
type: 'module',
});
let start = i * threadChunkCount;
let end = Math.min((i + 1) * threadChunkCount, chunkCount);
worker.postMessage({
file,
start,
end,
CHUNK_SIZE,
});
worker.onmessage = (e) => {
// 处理上传结果
};
}
(四)断点续传
-
记录上传进度 :在本地存储记录已上传的分片信息。
localStorage.setItem('uploadProgress', JSON.stringify({ file: file.name, uploaded: [] }));
-
恢复上传 :在上传中断后,读取本地存储的进度,跳过已上传的分片。
const uploadProgress = JSON.parse(localStorage.getItem('uploadProgress'));
if (uploadProgress && uploadProgress.file === file.name) {
for (let i = 0; i < uploadProgress.uploaded.length; i++) {
// 跳过已上传的分片
}
}
(五)文件合并
-
服务器合并 :服务器在接收到所有分片后,按照分片索引顺序合并文件。
-
前端确认 :前端在所有分片上传完成后,发送请求到服务器确认文件合并完成。
// 前端示例
fetch('/merge', {
method: 'POST',
body: JSON.stringify({ fileName: file.name }),
});
(六)上传进度与反馈
-
实时进度更新 :在上传过程中实时更新上传进度,并提供用户友好的界面反馈。
// 监听上传进度事件
worker.onmessage = (e) => {
const progress = (uploadedChunks / chunkCount) * 100;
console.log(`Upload progress: ${progress}%`);
};
-
错误处理与重试 :添加重试机制,处理上传过程中可能发生的网络错误。
let retryCount = 0;
const maxRetries = 3;
async function uploadChunk(chunk, index) {
try {
await fetch('/upload', { method: 'POST', body: chunk });
} catch (error) {
if (retryCount < maxRetries) {
retryCount++;
await uploadChunk(chunk, index);
} else {
console.error('Upload failed after multiple retries');
}
}
}
三、完整代码示例
(一)main.js
import { cutfile } from './cutfile.js';
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
console.time('cutfile');
cutfile(file).then((chunks) => {
console.timeEnd('cutfile');
console.log(chunks);
});
}
});
(二)cutfile.js
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB 每个分片的大小
export async function cutfile(file) {
const chunkCount = Math.ceil(file.size / CHUNK_SIZE); // 计算分片的数量
const result = [];
for (let i = 0; i < chunkCount; i++) {
const prom = createChunk(file, i, CHUNK_SIZE);
result.push(prom);
}
const THREAD_COUNT = navigator.hardwareConcurrency || 4; // 获取当前设备的线程数量
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT); // 计算每个线程处理的分片数量
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker('./worker.js', {
type: 'module',
});
let start = i * threadChunkCount; // 每个线程的起始分片下标
let end = Math.min((i + 1) * threadChunkCount, chunkCount); // 每个线程的结束分片下标
worker.postMessage({
file,
start,
end,
CHUNK_SIZE,
});
worker.onmessage = (e) => {
result.push(...e.data); // 将每个线程处理的分片结果添加到结果数组中
};
worker.onerror = (e) => {
console.error('Worker error:', e);
};
}
return result;
}
(三)createchunk.js
import { SparkMD5 } from './sparkmd5.js';
export function createChunk(file, index, chunkSize) {
return new Promise((resolve) => {
const start = index * chunkSize;
const end = start + chunkSize;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
const blob = file.slice(start, end);
fileReader.onload = () => {
spark.append(fileReader.result);
resolve({
start,
end,
index,
hash: spark.end(),
blob,
});
};
fileReader.readAsArrayBuffer(blob);
});
}
(四)worker.js
import { createChunk } from './createchunk.js';
onmessage = (e) => {
const { file, start, end, CHUNK_SIZE } = e.data;
const result = [];
for (let i = start; i < end; i++) {
const prom = createChunk(file, i, CHUNK_SIZE);
result.push(prom);
}
Promise.all(result).then((chunks) => {
postMessage(chunks);
});
};
四、总结与优化建议
(一)总结
本方案通过文件选择、分片处理、并发上传、断点续传、文件合并等步骤,实现了一个完整的大文件分片上传功能。利用 Web Workers 实现多线程处理,提高了上传效率和用户体验。
(二)优化建议
-
动态调整分片大小 :根据网络状况和设备性能动态调整分片大小,以达到最佳上传效果。
-
增强错误处理机制 :增加更详细的错误日志记录和用户提示,方便排查问题。
-
性能优化 :对代码进行进一步优化,减少不必要的计算和内存占用。