分片:
// 获取文件对象const inputFile = document.querySelector('input[type="file"]');// 设置分片大小:5MBconst 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);}