LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

分片上传 MD5 哈希校验:实现文件秒传,杜绝重复上传

admin
2026年7月6日 15:33 本文热度 82

前言:谁还没被重复上传折磨过?

不知道各位前端小伙伴有没有踩过这种崩溃大坑:好不容易选了一个 2G 的压缩包,吭哧吭哧传了 80%,手滑点了取消;转头重新选文件,只能从头开始分片上传,CPU 风扇呼呼狂转,你在工位想原地表演一个工位自闭。

更离谱的是:同一个安装包,你传给测试、传给产品、自己备份,要反复上传三四遍,服务器里堆一堆一模一样的文件分片,老板看存储账单都要皱眉:你们前端是拿服务器当网盘用了?

上次我们搞定了分片上传 + 断点续传,解决了「大文件传一半崩了要重来」的痛点;今天咱们用上 MD5 文件哈希校验,一次性搞定两大神仙功能:

✅ 文件秒传:服务器已经存过这个文件,直接 1 秒上传完成,不用传任何分片

✅ 去重上传:相同文件只存一份,彻底杜绝服务器重复文件占用存储

先划重点:MD5 不是加密,是文件「数字身份证」,哪怕文件名改了、后缀换了,只要文件内容一模一样,MD5 值就完全相同。

一、原理通俗唠明白:什么是文件 MD5 哈希?

举个生活化的例子:每个人都有唯一的身份证号,名字可以改、住址可以换,但身份证号不变;文件的 MD5 哈希,就是文件的「身份证号」。不管你把 demo.zip 重命名成 备份.zip,只要里面的字节内容一丝没变,MD5 值永远相同。

秒传 + 去重完整流程(一看就懂)

  1. 用户选择本地文件,前端读取整个文件,计算出唯一 MD5 哈希值
  2. 前端先拿着 MD5 去请求后端接口校验:这个文件咱们服务器存过吗?
  3. 情况 1:服务器存在该 MD5 → 直接返回上传成功,实现文件秒传,0 分片传输
  4. 情况 2:服务器不存在该 MD5 → 继续走我们之前的分片上传逻辑
  5. 每一个分片上传完成后,后端基于文件 MD5 合并分片,并且绑定文件唯一哈希,下次再传直接秒传

顺带还能顺便做断点续传校验:就算你只传了一半分片,下次上传同一个文件,前端通过 MD5 就能查到哪些分片已经上传过,只传缺失分片,双重保障不做无用功。

二、前置依赖:浏览器怎么计算文件 MD5?

我们用成熟稳定的开源库 spark-md5,轻量高效,专门用来计算大文件哈希,不会造成浏览器内存卡死。

1. 安装依赖

​npm install spark-md5
CDN 直接引入方式(不想用 npm 打包直接用这个)
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>

三、核心代码:分片计算 MD5(大文件友好,不卡顿)

⚠️ 划重点:千万别一次性读取整个大文件去计算 MD5,4G 以上文件会直接撑爆浏览器内存!正确方案:把文件切成小分片,循环读取分片累加计算最终哈希,丝滑不卡顿。

工具函数:文件分片计算 MD5

import SparkMD5 from 'spark-md5'/** * 分片计算文件MD5,避免大文件一次性读取内存溢出 * @param {File} file 本地选择的文件 * @param {Number} chunkSize 分片大小,默认2MB * @returns {Promise<string>} 文件唯一MD5哈希值 */function calculateFileMD5(file, chunkSize = 2 * 1024 * 1024) {  return new Promise((resolve) => {    const spark = new SparkMD5.ArrayBuffer()    const fileReader = new FileReader()    let currentChunk = 0    const totalChunks = Math.ceil(file.size / chunkSize)    // 分片循环读取    function loadNextChunk() {      const start = currentChunk * chunkSize      const end = start + chunkSize >= file.size ? file.size : start + chunkSize      const fileChunk = file.slice(start, end)      fileReader.readAsArrayBuffer(fileChunk)    }    // 读取分片成功,追加哈希计算    fileReader.onload = (e) => {      spark.append(e.target.result)      currentChunk++      if (currentChunk < totalChunks) {        // 还有分片继续读取        loadNextChunk()      } else {        // 所有分片读取完毕,返回最终MD5        const md5 = spark.end()        spark.destroy() // 释放内存        resolve(md5)      }    }    // 开始读取第一个分片    loadNextChunk()  })}

四、完整上传业务逻辑:MD5 预校验 + 分片上传

整体流程:选文件 → 计算 MD5 → 后端预校验 → 秒传 / 断点续传 / 全新分片上传

// 选择文件触发上传async function handleFileUpload(file) {  if (!filereturn alert('请先选择需要上传的文件')  try {    // 1. 耗时操作:计算文件MD5(大文件会等待几秒,建议加loading弹窗)    showLoading('正在解析文件,生成文件唯一ID...')    const fileMD5 = await calculateFileMD5(file)    // 2. 第一步:调用后端预校验接口,查询该MD5文件是否已上传    const checkRes = await axios.post('/api/upload/check-file', {      fileMD5,      fileName: file.name,      fileSize: file.size    })    // 场景1:服务器已经存在该文件,直接秒传成功    if (checkRes.data.code === 200 && checkRes.data.data.isExists) {      hideLoading()      alert(`🎉 文件秒传成功!该文件服务器已存在,无需重复上传,文件地址:${checkRes.data.data.fileUrl}`)      return    }    // 场景2:存在部分上传分片,走断点续传,只上传未上传过的分片    const uploadedChunkList = checkRes.data.data.uploadedChunkList || []    hideLoading()    // 调用我们上一篇写好的分片上传函数,跳过已上传分片    await uploadFileByChunk(file, fileMD5, uploadedChunkList)  } catch (err) {    hideLoading()    alert('文件上传异常:' + err.message)  }}

五、结合往期分片上传核心片段(关键改造点)

我们在之前的分片上传函数里,带上文件 MD5 作为文件夹唯一标识,相同文件的所有分片都会存在同一个 MD5 文件夹下:

async function uploadFileByChunk(file, fileMD5, uploadedChunkList) {  const chunkSize = 2 * 1024 * 1024 // 2MB每片  const totalChunks = Math.ceil(file.size / chunkSize)  // 并发控制,防止浏览器请求爆炸,限制最大并发数5  const requestTaskList = []  for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {    // 跳过已经上传成功的分片,断点续传核心    if (uploadedChunkList.includes(chunkIndex)) continue    const start = chunkIndex * chunkSize    const end = Math.min(start + chunkSize, file.size)    const chunkFile = file.slice(start, end)    const formData = new FormData()    formData.append('chunk', chunkFile)    formData.append('fileMD5', fileMD5) // 用MD5归类所有分片    formData.append('chunkIndex', chunkIndex)    formData.append('totalChunks', totalChunks)    formData.append('fileName', file.name)    // 单个分片上传请求    const uploadTask = axios.post('/api/upload/chunk', formData, {      onUploadProgress(e) => {        // 实时更新全局上传进度条        const singleProgress = e.loaded / e.total        const finishedCount = uploadedChunkList.length + requestTaskList.filter(item => item.finished).length        const totalProgress = ((finishedCount + singleProgress) / totalChunks * 100).toFixed(2)        updateProgressBar(totalProgress)      }    }).then(() => {      uploadTask.finished = true    })    requestTaskList.push(uploadTask)  }  // 等待所有分片上传完成  await Promise.all(requestTaskList)  // 通知后端合并所有分片,绑定MD5永久去重  const mergeRes = await axios.post('/api/upload/merge-chunk', {    fileMD5,    fileName: file.name,    totalChunks  })  alert(`✅ 文件全部上传合并完成,访问地址:${mergeRes.data.data.fileUrl}`)}

六、后端核心逻辑简要说明(前端小伙伴不用慌)

给后端同事看的极简业务逻辑,方便联调:

  1. 预校验接口 /api/upload/check-file
  • 根据前端传过来的fileMD5查询数据库
  • 如果数据库存在该 MD5 记录:返回文件已存在,携带文件访问地址,前端直接秒传
  • 如果不存在文件记录,但存在该 MD5 下的分片文件夹:返回已上传分片序号列表,前端断点续传
  • 完全没有任何记录:返回空数组,前端从头全部分片上传
  1. 分片上传接口:所有分片统一存放在 上传根目录/MD5值/ 文件夹下,保证相同文件分片不会混乱

  2. 分片合并接口:合并成功后,数据库存储 fileMD5、文件地址、文件名、文件大小 记录,作为后续秒传的唯一凭证

七、开发避坑小 Tips(血泪踩坑总结)

  1. ❌ 不要一次性读取超大文件计算 MD5,一定要分片累加计算,否则浏览器直接卡死崩溃
  2. ❌ MD5 只能用来做文件唯一性校验,不要用来做密码加密,安全性不够
  3. ✅ 上传时一定要把 MD5 传给后端,作为分片文件夹名,是断点续传 + 秒传的核心依据
  4. ✅ 计算 MD5 建议增加 loading 加载提示,超大文件可能需要 3-10 秒,别让用户以为页面卡死乱点
  5. ✅ 一定要做并发请求限制,一次性上传几百个分片会触发浏览器同源请求上限,直接大量请求失败


阅读原文:https://mp.weixin.qq.com/s/_zy6WU4_DRYgc6qxxyYqBw


该文章在 2026/7/6 15:33:43 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-2  粤公网安备44030602007207号