前言:谁还没被重复上传折磨过?
不知道各位前端小伙伴有没有踩过这种崩溃大坑:好不容易选了一个 2G 的压缩包,吭哧吭哧传了 80%,手滑点了取消;转头重新选文件,只能从头开始分片上传,CPU 风扇呼呼狂转,你在工位想原地表演一个工位自闭。
更离谱的是:同一个安装包,你传给测试、传给产品、自己备份,要反复上传三四遍,服务器里堆一堆一模一样的文件分片,老板看存储账单都要皱眉:你们前端是拿服务器当网盘用了?
上次我们搞定了分片上传 + 断点续传,解决了「大文件传一半崩了要重来」的痛点;今天咱们用上 MD5 文件哈希校验,一次性搞定两大神仙功能:
✅ 文件秒传:服务器已经存过这个文件,直接 1 秒上传完成,不用传任何分片
✅ 去重上传:相同文件只存一份,彻底杜绝服务器重复文件占用存储
先划重点:MD5 不是加密,是文件「数字身份证」,哪怕文件名改了、后缀换了,只要文件内容一模一样,MD5 值就完全相同。
一、原理通俗唠明白:什么是文件 MD5 哈希?
举个生活化的例子:每个人都有唯一的身份证号,名字可以改、住址可以换,但身份证号不变;文件的 MD5 哈希,就是文件的「身份证号」。不管你把 demo.zip 重命名成 备份.zip,只要里面的字节内容一丝没变,MD5 值永远相同。
秒传 + 去重完整流程(一看就懂)
- 用户选择本地文件,前端读取整个文件,计算出唯一 MD5 哈希值
- 前端先拿着 MD5 去请求后端接口校验:这个文件咱们服务器存过吗?
- 情况 1:服务器存在该 MD5 → 直接返回上传成功,实现文件秒传,0 分片传输
- 情况 2:服务器不存在该 MD5 → 继续走我们之前的分片上传逻辑
- 每一个分片上传完成后,后端基于文件 MD5 合并分片,并且绑定文件唯一哈希,下次再传直接秒传
顺带还能顺便做断点续传校验:就算你只传了一半分片,下次上传同一个文件,前端通过 MD5 就能查到哪些分片已经上传过,只传缺失分片,双重保障不做无用功。
二、前置依赖:浏览器怎么计算文件 MD5?
我们用成熟稳定的开源库 spark-md5,轻量高效,专门用来计算大文件哈希,不会造成浏览器内存卡死。
1. 安装依赖
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 { const md5 = spark.end() spark.destroy() resolve(md5) } } loadNextChunk() })}
四、完整上传业务逻辑:MD5 预校验 + 分片上传
整体流程:选文件 → 计算 MD5 → 后端预校验 → 秒传 / 断点续传 / 全新分片上传
async function handleFileUpload(file) { if (!file) return alert('请先选择需要上传的文件') try { showLoading('正在解析文件,生成文件唯一ID...') const fileMD5 = await calculateFileMD5(file) const checkRes = await axios.post('/api/upload/check-file', { fileMD5, fileName: file.name, fileSize: file.size }) if (checkRes.data.code === 200 && checkRes.data.data.isExists) { hideLoading() alert(`🎉 文件秒传成功!该文件服务器已存在,无需重复上传,文件地址:${checkRes.data.data.fileUrl}`) return } 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 const totalChunks = Math.ceil(file.size / chunkSize) 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) 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) const mergeRes = await axios.post('/api/upload/merge-chunk', { fileMD5, fileName: file.name, totalChunks }) alert(`✅ 文件全部上传合并完成,访问地址:${mergeRes.data.data.fileUrl}`)}
六、后端核心逻辑简要说明(前端小伙伴不用慌)
给后端同事看的极简业务逻辑,方便联调:
- 预校验接口
/api/upload/check-file
- 如果数据库存在该 MD5 记录:返回文件已存在,携带文件访问地址,前端直接秒传
- 如果不存在文件记录,但存在该 MD5 下的分片文件夹:返回已上传分片序号列表,前端断点续传
- 完全没有任何记录:返回空数组,前端从头全部分片上传
分片上传接口:所有分片统一存放在 上传根目录/MD5值/ 文件夹下,保证相同文件分片不会混乱
分片合并接口:合并成功后,数据库存储 fileMD5、文件地址、文件名、文件大小 记录,作为后续秒传的唯一凭证
七、开发避坑小 Tips(血泪踩坑总结)
- ❌ 不要一次性读取超大文件计算 MD5,一定要分片累加计算,否则浏览器直接卡死崩溃
- ❌ MD5 只能用来做文件唯一性校验,不要用来做密码加密,安全性不够
- ✅ 上传时一定要把 MD5 传给后端,作为分片文件夹名,是断点续传 + 秒传的核心依据
- ✅ 计算 MD5 建议增加 loading 加载提示,超大文件可能需要 3-10 秒,别让用户以为页面卡死乱点
- ✅ 一定要做并发请求限制,一次性上传几百个分片会触发浏览器同源请求上限,直接大量请求失败
阅读原文:https://mp.weixin.qq.com/s/_zy6WU4_DRYgc6qxxyYqBw
该文章在 2026/7/6 15:33:43 编辑过