浏览器中计算大文件SHA-256哈希

754 阅读5分钟

在浏览器中计算大文件(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、性能优化建议

  1. 合理设置块大小:通常1MB左右的块大小在性能和内存使用之间取得良好平衡
  2. 使用Web Worker:将哈希计算放到Web Worker中,避免阻塞主线程
  3. 进度反馈:在分块处理时可以提供进度反馈,改善用户体验
  4. 内存管理:及时释放不再使用的内存