如有不妥,多多指教!
-
整个文件直接计算MD5
import SparkMD5 from 'spark-md5'
const spark = new SparkMD5.ArrayBuffer()
export const createMD5 = (file: File) => {
// 验证file
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onerror = (e) => {
console.error(e);
return reject(e);
};
fileReader.onload = () => {
try {
resolve(spark.hash(dataBuffer));
} catch (e) {
console.error(e);
return reject(e);
}
};
});
};
直接加载整个文件,将整个文件的二进制数据arrayBuffer丢给计算工具计算MD5,如果是一个小文件就还好,但是大文件,批量的文件,一下子将大量资源放到内存里会造成明显的卡顿 .
使用
const uploadFolder = async (file: any) => {
try {
let md5 = await createMD5(file);
console.log(md5);
} catch (e) {
// ElMessage.error('上传失败,请重试!');
}
};
-
将文件分片后计算MD5
utils/getMD5.ts
import SparkMD5 from 'spark-md5'
export const BASE_SIZE: number = 1024 * 1024; // 1MB
// 将文件切片,返回切片后的分片list
export const createFileChunk = (file: File): Blob[] => {
const chunkSize: number = getChunkSize(file);
const chunks: Blob[] = [];
let startPos = 0;
while (startPos < file.size) {
chunks.push(file.slice(startPos, startPos + chunkSize));
startPos += chunkSize;
}
return chunks;
};
// 获取分片大小
export const getChunkSize = (file: File): number => {
const fileSize = file.size; // 文件大小
let chunkSize: number;
if (fileSize <= 5 * BASE_SIZE) {
// 0-5M,不分片
chunkSize = fileSize;
} else if (fileSize <= 20 * BASE_SIZE) {
// 5-20M,每个分片大小1M
chunkSize = BASE_SIZE;
} else if (fileSize <= 50 * BASE_SIZE) {
// 20-50M,每个分片大小2M
chunkSize = 2 * BASE_SIZE;
} else if (fileSize <= 100 * BASE_SIZE) {
// 50-100M,每个分片大小4M
chunkSize = 4 * BASE_SIZE;
} else if (fileSize <= 200 * BASE_SIZE) {
// 100-200M,每个分片大小6M
chunkSize = 6 * BASE_SIZE;
} else {
// 每个分片大小8M,不分多了,因为一个 几十M 上传也很慢
chunkSize = 8 * BASE_SIZE;
}
// 根据自己的需求修改计算分片
// 因为网速慢,我在计算分片设置不大,分片上传的时候,一片一个请求也会快一些,就是请求会变多
// else if (fileSize <= 500 * BASE_SIZE) {
// // 200-500M,每个分片大小10M
// chunkSize = 10 * BASE_SIZE;
// } else if (fileSize <= 1024 * BASE_SIZE) {
// // 500M-1G,每个分片大小20M
// chunkSize = 20 * BASE_SIZE;
// } else if (fileSize <= 2 * 1024 * BASE_SIZE) {
// // 1G-2G,每个分片大小30M
// chunkSize = 30 * BASE_SIZE;
// } else if (fileSize <= 4 * 1024 * BASE_SIZE) {
// // 2G-4G,每个分片大小40M
// chunkSize = 40 * BASE_SIZE;
// } else {
// // 4G以上,每个分片大小50M
// chunkSize = 50 * BASE_SIZE;
// }
return chunkSize;
};
// 计算文件的MD5值
export const getFileMD5 = (chunksList: Blob[]): Promise<any> => {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
// 当前分片下标
let currentChunk = 1;
// 分片总数(向下取整)
const chunks = chunksList.length;
// MD5加密开始时间
const startTime = new Date().getTime();
loadNext();
// fileReader.readAsArrayBuffer操作会触发onload事件
fileReader.onload = function (e) {
console.log('currentChunk :>> ', currentChunk);
if (e.target && e.target.result instanceof ArrayBuffer) {
spark.append(e.target.result);
} else {
console.error('Failed to read file as ArrayBuffer: ');
// 在这里可以添加更多的错误处理逻辑
}
if (currentChunk < chunks) {
console.log(currentChunk, chunks);
currentChunk++;
loadNext();
} else {
// 该文件的md5值
const md5 = spark.end();
console.log(`MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`);
// 回调传值md5
resolve(md5);
}
};
fileReader.onerror = function () {
// ElMessage({
// message: '文件读取错误,请重试',
// type: 'error',
// });
resolve('');
};
// 加载下一个分片
function loadNext() {
// 文件分片操作,读取下一分片(fileReader.readAsArrayBuffer操作会触发onload事件)
fileReader.readAsArrayBuffer(chunksList[currentChunk - 1]);
}
});
};
使用
import { createFileChunk, getFileMD5 } from '@/utils/getMD5.ts';
const uploadFolder = async (file: any) => {
const chunksList: Blob[] = createFileChunk(file);
try {
let md5 = await getFileMD5(chunksList);
console.log(md5);
// 根据md5和chunksList分片,可以做分片上传了
// ...
// ...
} catch (e) {
// ElMessage.error('上传失败,请重试!');
}
};
大文件也不会页面卡顿,只是计算大文件 md5 的时间还是会很久,loading的状态会有所保持.
1.1G 大小文件,25s左右(可能电脑不一样速度不一样哈~此处是我的电脑时长)
-
Web Worker优化计算文件MD5
utils/getMD5.ts
import pLimit from 'p-limit';
// BlockSize 块大小 (默认 50MB)
// ChunkSize 分片大小 (默认 10MB)
const BlockSize = 50 * 1024 * 1024;
const ChunkSize = 10 * 1024 * 1024;
// 设置最大 Worker 数量
export const MAX_WORKERS = 4;
// 获取当前机器的CPU的逻辑核数:(12个-自己电脑),但是我试了一下,4个目前是自己电脑更快的
// export const MAX_WORKERS = navigator.hardwareConcurrency;
// 获取md5,通过webworker形式提高效率,这种 MD5值 不是文件的md5(因为计算的文件分块 md5 ,再做的合并),但是依然可以作为唯一值来给后端,做分片上传的唯一标识
export const getNewFileMD5 = async (file: File, max_worker = MAX_WORKERS) => {
if (!file) throw new Error('File is required');
// MD5加密开始时间
const startTime = new Date().getTime();
let md5 = '';
const blocks = createChunks(file); // 创建块和分片
const limit = pLimit(max_worker);
// limit接受一个异步任务
const workerPromises = blocks.map((block) => {
return limit(
() =>
new Promise((workerResolve, workerReject) => {
const worker = new Worker(new URL('./hashWorker.ts', import.meta.url));
const chunkArray = createChunksFromBlock(block);
// 将块中的所有片发送给 Worker
worker.postMessage({ chunks: chunkArray });
worker.onmessage = (event) => {
if (event.data.hash) {
workerResolve(event.data.hash); // 返回当前块的哈希
worker.terminate(); // 销毁worker
} else if (event.data.error) {
workerReject(event.data.error);
worker.terminate();
}
};
worker.onerror = (error) => {
workerReject('Worker error: ' + error.message);
worker.terminate();
};
}),
);
});
// 等待所有块的哈希返回
const hashes: string[] = (await Promise.all(workerPromises)) as unknown as string[];
console.log('所有块的哈希', hashes);
// 合并哈希值
const finalSpark = new SparkMD5();
hashes.forEach((hash) => finalSpark.append(hash));
md5 = finalSpark.end(); // 计算最终的 MD5 值
// 这种 MD5值 不是文件的md5,但是依然可以作为唯一值来给后端,做分片上传的唯一标识
console.log(`worker数量:${max_worker},MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`);
return md5;
};
// 创建块
export const createChunks = (file: File) => {
const blocks = [];
let cur = 0;
// 分块
while (cur < file.size) {
const block = file.slice(cur, cur + BlockSize);
blocks.push(block); // 保存块本身以计算字节数
cur += BlockSize;
}
return blocks;
};
// 从块中创建分片
export const createChunksFromBlock = (block: Blob) => {
const result = [];
let cur = 0;
while (cur < block.size) {
const chunk = block.slice(cur, cur + ChunkSize);
result.push(chunk);
cur += ChunkSize;
}
return result;
};
utils/hashWorker.ts
import SparkMD5 from 'spark-md5';
self.onmessage = function (event) {
const chunks = event.data.chunks;
const spark = new SparkMD5.ArrayBuffer();
let loaded = 0;
const readChunk = (index: number) => {
if (index >= chunks.length) {
// 计算完毕
self.postMessage({ hash: spark.end() });
return;
}
const fileReader = new FileReader();
fileReader.onload = (e: any) => {
spark.append(e.target.result);
loaded++;
readChunk(index + 1);
};
fileReader.onerror = () => {
self.postMessage({ error: 'File reading error' });
};
fileReader.readAsArrayBuffer(chunks[index]);
};
readChunk(0);
};
使用
import { createFileChunk, getNewFileMD5 } from '@/utils/getMD5.ts';
const uploadFolder = async (file: any) => {
const chunksList: Blob[] = createFileChunk(file);
try {
let md5 = await getNewFileMD5(file);
console.log(md5);
// 根据md5和chunksList分片,可以做分片上传了
// ...
// ...
// 根据 md5 查询哪些分片上传过了,返回list
// 剔除已上传的,剩下未上传的 residueChunksList
// residueChunksList 遍历调用分片上传接口
} catch (e) {
// ElMessage.error('上传失败,请重试!');
}
};
获取md5,通过webworker形式提高效率,这种 MD5值 不是文件的md5(因为计算的文件分块 md5 ,再做的合并),但是依然可以作为唯一值来给后端,做分片上传的唯一标识.
1.1G 大小文件,5s左右,显著提升(可能电脑不一样速度不一样哈~此处是我的电脑时长)
分片批量上传
import pLimit from 'p-limit';
// 仅供参考,每个后台设计的接口也不一样
export const batchUpload = async (uploadObj: Record<string, any>, pLimitNum: number = 5) => {
// 定义并发数量
const limit = pLimit(pLimitNum);
// limit接受一个异步任务
const input = uploadObj.chunksList.map((item: any) => {
const formData = new FormData();
formData.append('file', item.file);
formData.append('fileMd5', uploadObj.fileMd5);
formData.append('fileTotalChunks', uploadObj.fileTotalChunks);
formData.append('blockIndex', item.blockIndex);
formData.append('fileName', uploadObj.fileName);
return limit(
() =>
new Promise(async (resolve) => {
// 调用上传分片接口
const res = await Upload(formData);
// ...
// ...
resolve(res);
}),
);
});
console.log(input);
// 使用Promise.all来接收异步任务列表
return await Promise.all(input);
};