在浏览器中计算大文件(10-50MB及以上)的SHA-256哈希值时,直接使用crypto-js等库可能会导致内存溢出或页面崩溃,这是因为这些库通常需要将整个文件加载到内存中进行处理。以下是几种可行的解决方案:
1、使用Web Crypto API + 分块处理
现代浏览器都内置了Web Crypto API,它提供了原生的加密功能,且支持流式处理大文件。
async function calculateFileHash(file: File): Promise<string> {
// 读取文件为ArrayBuffer
const arrayBuffer = await file.arrayBuffer();
// 使用Web Crypto API计算哈希
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
// 将哈希值转换为十六进制字符串
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
2、分块读取文件计算哈希(推荐)
对于超大文件,最佳实践是分块读取文件并逐步更新哈希值,这样可以避免一次性加载整个文件到内存中。
async function calculateLargeFileHash(file: File, chunkSize = 1024 * 1024): Promise<string> {
// 初始化Web Crypto API的哈希计算器
const hash = await crypto.subtle.createHash('SHA-256');
// 分块处理文件
let offset = 0;
while (offset < file.size) {
// 计算当前块的结束位置
const end = Math.min(offset + chunkSize, file.size);
// 读取文件块
const chunk = file.slice(offset, end);
const chunkBuffer = await chunk.arrayBuffer();
// 更新哈希计算
hash.update(new Uint8Array(chunkBuffer));
// 更新偏移量
offset = end;
}
// 获取最终哈希值
const hashBuffer = await hash.digest();
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
3、使用FileReader和流式处理
对于不支持Web Crypto API的旧浏览器,可以使用FileReader进行分块读取:
function calculateHashWithFileReader(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const chunkSize = 1024 * 1024; // 1MB chunks
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = function(e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = function() {
reject('文件读取错误');
};
function loadNext() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
fileReader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}
4、使用第三方库(如hash-wasm)
hash-wasm是一个高性能的哈希计算库,支持WebAssembly,可以高效处理大文件:
import { createSHA256 } from 'hash-wasm';
async function calculateHashWithWasm(file: File): Promise<string> {
const sha256 = await createSHA256();
const chunkSize = 1024 * 1024; // 1MB chunks
let offset = 0;
while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize);
const chunkBuffer = await chunk.arrayBuffer();
sha256.update(new Uint8Array(chunkBuffer));
offset += chunkSize;
}
return sha256.digest('hex');
}
5、完整示例(Web Crypto API + 分块处理 + 进度反馈)
async function calculateFileHashWithProgress(
file: File,
onProgress?: (progress: number) => void,
chunkSize = 1024 * 1024
): Promise<string> {
// 初始化哈希计算器
const hash = await crypto.subtle.createHash('SHA-256');
let offset = 0;
while (offset < file.size) {
const end = Math.min(offset + chunkSize, file.size);
const chunk = file.slice(offset, end);
const chunkBuffer = await chunk.arrayBuffer();
// 更新哈希计算
hash.update(new Uint8Array(chunkBuffer));
// 更新偏移量
offset = end;
// 报告进度
if (onProgress) {
onProgress((offset / file.size) * 100);
}
}
// 获取最终哈希值
const hashBuffer = await hash.digest();
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
// 使用示例
const fileInput = document.getElementById('file-input') as HTMLInputElement;
fileInput.addEventListener('change', async (e) => {
const file = fileInput.files?.[0];
if (!file) return;
try {
const hash = await calculateFileHashWithProgress(
file,
(progress) => {
console.log(`计算进度: ${progress.toFixed(1)}%`);
}
);
console.log('文件哈希值:', hash);
} catch (error) {
console.error('哈希计算失败:', error);
}
});
/**
* 计算大文件SHA-256哈希值(分块处理)
* @param {File} file - 要计算哈希的文件对象
* @param {Object} [options] - 配置选项
* @param {number} [options.chunkSize=4 * 1024 * 1024] - 分块大小(字节),默认4MB
* @param {function} [options.onProgress] - 进度回调函数
* @returns {Promise<string>} - 返回Promise,解析为十六进制哈希字符串
*/
async function computeFileSHA256(file, options = {}) {
const {
chunkSize = 4 * 1024 * 1024, // 默认4MB分块
onProgress
} = options;
// 创建SHA-256哈希计算器
const hash = await crypto.subtle.createHash('SHA-256');
const totalChunks = Math.ceil(file.size / chunkSize);
let processedChunks = 0;
// 分块处理文件
for (let start = 0; start < file.size; start += chunkSize) {
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
// 读取当前分块
const chunkBuffer = await readFileChunk(chunk);
// 更新哈希计算
hash.update(chunkBuffer);
processedChunks++;
// 触发进度回调
if (onProgress) {
onProgress({
loaded: end,
total: file.size,
progress: (processedChunks / totalChunks) * 100
});
}
}
// 获取最终哈希值
const hashBuffer = await hash.digest();
return bufferToHex(hashBuffer);
}
/**
* 读取文件分块为ArrayBuffer
* @param {Blob} chunk - 文件分块
* @returns {Promise<Uint8Array>}
*/
function readFileChunk(chunk) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.onerror = reject;
reader.readAsArrayBuffer(chunk);
});
}
/**
* 将ArrayBuffer转换为十六进制字符串
* @param {ArrayBuffer} buffer
* @returns {string}
*/
function bufferToHex(buffer) {
return Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// 使用示例
// 获取文件输入元素
const fileInput = document.querySelector('input[type="file"]');
const progressElement = document.getElementById('progress');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
console.log(`开始计算 ${formatFileSize(file.size)} 文件的哈希...`);
const hash = await computeFileSHA256(file, {
onProgress: ({ progress }) => {
progressElement.textContent = `计算进度: ${progress.toFixed(1)}%`;
}
});
console.log('SHA-256哈希值:', hash);
progressElement.textContent = `计算完成: ${hash}`;
} catch (error) {
console.error('哈希计算失败:', error);
progressElement.textContent = '计算失败';
}
});
// 辅助函数:格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
批量
import CryptoJS from 'crypto-js';
/**
* 创建哈希计算Worker
* @returns 返回配置好的Web Worker实例
*/
function createHashWorker(): Worker {
const workerCode = `
self.importScripts('https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js');
self.onmessage = async function(e) {
try {
// 参数验证
if (!e.data || !e.data.file) {
throw new Error('未接收到有效的文件数据');
}
const { file, algorithm } = e.data;
if (!(file instanceof Blob)) {
throw new Error('文件类型不正确,必须是Blob对象');
}
// 动态调整分块大小 (1MB-10MB之间)
const chunkSize = Math.min(
Math.max(Math.floor(file.size / 100), 1024 * 1024),
1024 * 1024 * 10
);
let hasher;
// 初始化哈希器
switch(algorithm) {
case 'MD5': hasher = CryptoJS.algo.MD5.create(); break;
case 'SHA1': hasher = CryptoJS.algo.SHA1.create(); break;
case 'SHA256': hasher = CryptoJS.algo.SHA256.create(); break;
default: throw new Error('不支持的哈希算法');
}
// 分块处理文件
let offset = 0;
while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize);
const arrayBuffer = await chunk.arrayBuffer();
const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer);
hasher.update(wordArray);
offset += chunkSize;
// 发送进度信息
self.postMessage({
type: 'progress',
progress: Math.min(offset / file.size, 1)
});
}
// 计算最终哈希
const hash = hasher.finalize().toString();
self.postMessage({ type: 'complete', hash });
} catch (error) {
self.postMessage({
type: 'error',
error: error.message || '哈希计算过程中发生未知错误'
});
}
}
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
}
/**
* 计算文件哈希(自动判断是否使用Worker)
* @param file 文件对象
* @param algorithm 哈希算法 (MD5|SHA1|SHA256)
* @param threshold 使用Worker的阈值(默认20MB)
* @returns Promise<string> 返回哈希字符串
* @throws 当文件过大或参数错误时抛出异常
*/
export async function getFileHashWithWorker(
file: File,
algorithm: 'MD5' | 'SHA1' | 'SHA256' = 'SHA256',
threshold: number = 20 * 1024 * 1024
): Promise<string> {
// 参数验证
if (!file) {
throw new Error('文件参数不能为空');
}
// 文件大小限制检查 (最大1GB)
const MAX_FILE_SIZE = 1024 * 1024 * 1024;
if (file.size > MAX_FILE_SIZE) {
throw new Error(`文件大小超过限制 (${MAX_FILE_SIZE / (1024 * 1024)}MB)`);
}
// 算法支持检查
const validAlgorithms = ['MD5', 'SHA1', 'SHA256'];
if (!validAlgorithms.includes(algorithm)) {
throw new Error(`不支持的哈希算法: ${algorithm}`);
}
// 小文件直接在主线程处理
if (file.size <= threshold) {
try {
const arrayBuffer = await file.arrayBuffer();
const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer);
switch (algorithm) {
case 'MD5': return CryptoJS.MD5(wordArray).toString();
case 'SHA1': return CryptoJS.SHA1(wordArray).toString();
case 'SHA256': return CryptoJS.SHA256(wordArray).toString();
default: return CryptoJS.SHA256(wordArray).toString();
}
} catch (error) {
throw new Error(`主线程哈希计算失败`);
}
}
// 大文件使用Worker处理
return new Promise((resolve, reject) => {
const worker = createHashWorker();
let timeoutId: NodeJS.Timeout;
// 设置超时处理 (5分钟)
timeoutId = setTimeout(() => {
worker.terminate();
reject(new Error('哈希计算超时 (5分钟)'));
}, 5 * 60 * 1000);
worker.postMessage({ file, algorithm });
worker.onmessage = (e) => {
if (e.data.type === 'complete') {
clearTimeout(timeoutId);
worker.terminate();
resolve(e.data.hash);
} else if (e.data.type === 'error') {
clearTimeout(timeoutId);
worker.terminate();
reject(new Error(`Worker计算错误: ${e.data.error}`));
}
// progress消息可以在这里处理UI更新
};
worker.onerror = (err) => {
clearTimeout(timeoutId);
worker.terminate();
reject(new Error(`Worker发生错误: ${err.message}`));
};
});
}
// use
const hashFiles: HashFile[] = await Promise.all(files.map(async (file) => {
try {
const hash = await getFileHashWithWorker(file, 'SHA256', 10 * 1024 * 1024);
// const hash = await getFileHash(file);
console.log(hash)
// TODO
return { hash, file };
} catch (err) {
// 哈希计算失败 - 直接上传
console.error(file.name, err);
throw err;
}
}
通过以上方法,你可以在浏览器中安全高效地计算大文件的SHA-256哈希值,而不会导致内存溢出或页面崩溃。
6、性能优化建议
- 合理设置块大小:通常1MB左右的块大小在性能和内存使用之间取得良好平衡
- 使用Web Worker:将哈希计算放到Web Worker中,避免阻塞主线程
- 进度反馈:在分块处理时可以提供进度反馈,改善用户体验
- 内存管理:及时释放不再使用的内存