大文件分片上传
分片:
// 获取文件对象
const inputFile = document.querySelector('input[type="file"]');
// 设置分片大小:5MB
const CHUNK_SIZE = 5 * 1024 * 1024;
// 文件上传事件
inputFile.onchange = async (e) => {
// 获取文件信息
const file = e.target.files[0];
// 获取文件分片信息
const chunks = await cutFile(file, CHUNK_SIZE);
};
// 获取文件所有分片信息
async function cutFile(file, chunkSize = 5 * 1024 * 1024) {
// 计算分片数量:文件大小除以分片大小然后在向上取整
const chunkCount = Math.ceil(file.size / chunkSize);
// 获取所有分片信息
let chunks = [];
for (let i = 0; i < chunkCount; i++) {
// 获取单个分片信息
const chunk = await createChunk(file, i, chunkSize);
chunks.push(chunk);
}
return chunks;
}
// 获取文件单个分片信息
import SparkMD5 from "sparkmd5.js"; // 计算hash值的第三方库
async function createChunk(file, index, chunkSize = 5 * 1024 * 1024) {
return new Promise((resolve) => {
const start = index * chunkSize; // 当前分片的起始字节位置
const end = Math.min(start + chunkSize, file.size); // 当前分片的结尾字节位置
const blob = file.slice(start, end); // 分片内容
const spark = new SparkMD5.ArrayBuffer(); // 创建处理hash值的实例对象,使用二进制数据获取MD5的值
const fileReader = new FileReader(); // 创建浏览器内置的文件读取器,将二进制片段读取成内存中的数据
// 绑定文件读取器,读取完成事件
fileReader.onload = (e) => {
spark.append(e.target.result); // 将二进制数据的结果追加到MD5的计算缓存中
resolve({
index,
start,
end,
blob,
hash: spark.end() // 计算并返回当前分片的hash值
});
};
// 文件读取器开始读取分片数据
fileReader.readAsArrayBuffer(blob);
});
}
优化:由于计算hash值是一个运算过程(CPU 密集型任务)很耗时并且js是单线程语言,所以必须要算完一个分片后 CPU 存在空闲才能去算下一个分片的 hash 值。
- 如何解决:IO密集型操作可以用并发处理(promise.all),CPU密集型任务只能开多线程优化。
// 获取计算机内核数量 - 线程数量
const THREAD_COUNT = navigator.hardwareConcurrency || 4;
// 获取文件所有分片信息
async function cutFile(file, chunkSize = 5 * 1024 * 1024) {
return new Promise((resolve) => {
// 计算分片数量:文件大小除以分片大小然后在向上取整
const chunkCount = Math.ceil(file.size / chunkSize);
// 计算每个线程可以分配分片的数量:总的分片数量除以总的线程数量然后在向上取整
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT);
// 开启其他线程任务
const result = [];
let finishCount = 0;
for (let i = 0; i < THREAD_COUNT; i++) {
// 开启其他线程,并设置线程是一个module模块,支持导入
const worker = new Worker("./worker.js", { type: "module" });
// 向其他线程传递消息
const start = i * threadChunkCount;
const end = Math.min(start + threadChunkCount, chunkCount);
worker.postMessage({
file,
start,
end,
chunkSize
});
// 从其他线程获取消息,确保接收到的信息顺序是正确的,则使用下标接收信息
worker.onmessage = (e) => {
worker.terminate(); // 结束当前线程
result[i] = e.data; // 当前线程内的所有分片信息都保存起来
finishCount++;
// 当其他线程全部结束则返回所有分片信息,扁平化数组
if (finishCount === THREAD_COUNT) resolve(result.flat());
};
}
});
}
// 其他线程内部执行的内容:worker.js中
onmessage = async (e) => {
const { file, start, end, chunkSize } = e.data; // 获取主线程传递的消息
// 获取当前线程内部的所有分片信息
let result = [];
for (let i = 0; i < end; i++) {
// 获取单个分片信息
const prom = createChunk(file, i, chunkSize);
result.push(prom);
}
const chunks = await Promise.all(result); // 等待分片信息全部生成完成
postMessage(chunks); // 向主线程传递当前线程分片信息
};
断点续传:在本地存储中保存已上传的信息,下次重新上传的时候去检查本地记录,存在则跳过当前分片
async function resumeUpload(file) {
const chunkSize = 5 * 1024 * 1024;
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = generateFileId(file.name, file.size);
// 1. 首先检查本地存储的上传记录
let uploadedChunks = getLocalUploadProgress(fileId) || [];
// 2. 如果本地没有记录,再请求服务器验证
if (uploadedChunks.length === 0) {
try {
const response = await axios.get(`/upload-status?fileId=${fileId}`);
uploadedChunks = response.data.uploadedChunks || [];
// 将服务器记录保存到本地
saveLocalUploadProgress(fileId, uploadedChunks);
} catch (error) {
console.error("获取上传状态失败,从头开始上传", error);
uploadedChunks = [];
}
}
// 3. 上传缺失的分片
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.includes(i)) {
console.log(`分片 ${i} 已上传,跳过`);
continue;
}
const chunk = file.slice(i * chunkSize, Math.min(file.size, (i + 1) * chunkSize));
const formData = createFormData(chunk, i, totalChunks, fileId);
try {
await axios.post("/upload-chunk", formData);
// 更新本地记录
uploadedChunks.push(i);
saveLocalUploadProgress(fileId, uploadedChunks);
} catch (error) {
console.error(`分片 ${i} 上传失败:`, error);
// 保留已成功上传的分片记录
throw error;
}
}
// 4. 所有分片上传完成后合并
await axios.post("/merge-chunks", { fileId, fileName: file.name });
// 清除本地记录
clearLocalUploadProgress(fileId);
}
秒传:通过接口传入 hash 值判断是否服务器是否存在文件,存在就复用分片
async function quickUpload(file) {
// 检查服务器是否已有该文件
const { exists } = await axios.post("/check-file", {
fileName: file.name,
fileSize: file.size,
fileHash
});
if (exists) {
console.log("文件已存在,秒传成功");
return { skipped: true };
}
// 不存在则正常上传
return uploadFile(file, fileHash);
}
注意:需要自己理解,这里只是示例
本文来自博客园,作者:小周同学~,转载请注明原文链接:https://www.cnblogs.com/xiaozhou-wuyu/p/19099312

浙公网安备 33010602011771号