react-antd实现 大文件上传 + 断点续传
文件太大分片上传能加快上传速度,提高用户体验能如果上次上传失败或者中途离开的话下一次上传过的就不用重头开始了已经上传过的文件根据HASH查询直接秒传。
目录
6.1.1 使用new关键字创建FileReader实例的主要原因包括:
6.1.2 // 获取文件切片 this.getSliceFile()方法的核心逻辑:
1 大文件上传优点:
- 文件太大分片上传能加快上传速度,提高用户体验
- 能断点续传 如果上次上传失败或者中途离开的话下一次上传过的就不用重头开始了
- 已经上传过的文件根据HASH查询直接秒传
2 大文件上传缺点:
1.后台可能设置了请求时长限制,太久会上传失败(解决:后端不设置上传时长)
2.NGINX可能设置了文件上传的最大限制导致失败(解决:比如分片25M,nginx设置文件上传最大限度50M)
3 大文件上传原理:
- 用户选择要上传的大文件,计算整个文件的MD5。
- 前端根据分片大小将文件切分成多个小块,计算每个分片文件的MD5。
- 逐个上传每个小块到服务器端。
- 服务器端接收并保存每个小块。
- 在服务器端,根据上传的小块将它们合并成完整的文件。
4 为什么要用md5
因为每个文件都会有自己专属独立的md5值,就像是每个人的身份证,比如我们在某个平台发布视频,将视频文件二次上传的时候就会遇到不容易过审的原因,同一个MD5就有很大的机率显示搬运被退回。刚好后端同学也可以通过MD5这种特性来判断上传的文件是否完整。
如何快速计算文件的 md5 值呢? 我们使用 js-spark-md5 这个库
5 实现流程:
在upload组件上传文件的钩子函数beforeUpload() 中:
- 获取切片文件:设置切片文件大小、每次上传的开始字节,每次上传的结尾字节。文件切片的核心是使用Blob 对象的 slice 方法:
start 和 end 代表 Blob 里的下标,表示被拷贝进新的 Blob 的字节的起始位置和结束位置。contentType 会给新的 Blob 赋予一个新的文档类型,很少使用。var blob = file.slice([start [, end [, contentType]]]};- 计算分片文件的MD5
- 上传分片文件,断点续传(如何实现断点续传,关键点是后端需要记录文件文件切片的信息。用户在上传一个文件之前,先询问服务器,当前文件是否存在已经上传完毕的切片,如果存在的话,需要返回切片信息。前端根据返回的信息,调整当前的进度,上传未完成的切片)
- 检验分片数量及上传的结果,全部上传,文件合并:
- 前端发送切片完成后,发送一个合并请求,后端收到请求后,将之前上传的切片文件合并。(上面展示的代码采用 这个)
- 后台记录切片文件上传数据,当后台检测到切片上传完成后,自动完成合并。
- 创建一个和源文件大小相同的文件,根据切片文件的起止位置直接将切片写入对应位置。
5.1 功能流程图:
sequenceDiagram
前端->>后端: 1. 计算文件MD5并校验
后端-->>前端: 返回fileId和断点位置
前端->>前端: 2. 分片文件(25MB/片)
loop 分片上传
前端->>后端: 3. 上传分片(含序号+MD5)
后端-->>前端: 返回分片结果
end
前端->>后端: 4. 请求合并文件
后端-->>前端: 返回最终结果
6 beforeUpload 部分代码1:
export default class Project extends React.PureComponent { construction (props){ this.state = { file: {}, fileplanNumber: 0, // 上传文件进度的百分比 fileUploadState: '', // 上传文件的状态,fail失败, success成功 interFaceStart: 0, // 文件上传时的分片文件下标, 作用于断点续传 } } beforeUpload = (file) => { this.setState({ file, // 把file存起来 }) let reader = new FileReader(); // 用于异步读取文件内容(如用户上传的二进制文件) let md5 = ''; reader.onload = (e) => { // onload:文件读取完成时触发,通过 e.target.result 获取内容 const spark = new SparkMD5.ArrayBuffer; // 计算文件的MD5哈希值(用于文件唯一标识或秒传校验) spark.append(e.target.result); // 添加文件二进制数据 md5 = spark.end(); // 生成最终MD5值 axios.post('', { // 发送MD5校验请求 md5, id, // 后端需要的 }).then((res) => { const { fileId, start, finish, message } = res.data.data; if(res.data.code !== 200){ message.error(message); } if(finish && finish === 'true'){ this.setState({ file: {}, // 等于true,表示上传完成了 fileplanNumber: 0, // 上传文件进度的百分比 }) message.error(message); return; } // 请求成功后 this.setState({ fileId, interFaceStart: start || 0, // 文件上传时,分片文件下标,为了断点续传 }, () => { this.getSliceFile() // 获取文件切片 }) }) } reader.readArrayBufffer(file); // 将文件读取为 ArrayBuffer 格式,处理二进制数据 return false; } // 获取文件切片 getSliceFile = async () => { const { search, cataList } = this.props; const { file, fileId, interFaceStart } = this.state; const archiveId = cataList && cataList[cataList.length - 1].archiveId; const fileSize = file.size; // 文件的大小 const piece = 1024 *1024 * 25; // 分片大小,每片25M let start = 0; // 每次上传开始字节 let index = 1; let end = start + piece; // 每次上传的结尾字节 const chunksList = []; while(start < fileSize){ const current = Math.min(end, fileSize) // 两者中取最小的 const blob = file.slice(file, start, current); // 计算分片文件的MD5 let sliceFileMD5 = ''; sliceFileMD5 = await this.getSliceFileMD5(blob ); // 拼接分片信息数据 chunksList.push({ file: blod, index, sliceFileMD5, }); start = current; end = start + piece; index += 1; } // 检验分片数量,开始上传 if (chunksList && chunksList.length) { const chunks = chunksList.slice(interFaceStart); // 分片上传的结果 let resultList = 0; // 循环分片数据 for(const item of chunks){ // 调用接口上传分片内容 const resultFile = await this.uploadSliceFile(item, chunks); //记录上传结果 resultList += 1; // 一次失败,结束后续上传 if(!resultFile){ break; } } // 检验分片数量,分片上传结果数量,全部上传完成,调用合并接口 if(resultList === chunks.length){ const { fileList } = this.state; axios.post('', { fileId, fileName: file.name, businessId: archiveId, businessType: 'archive', }).then((res) => { if(res.data.code !== 200){ message.error(res.data.message) } message.success('上传成功!') // 调接口,刷新页面 axios.post('', { pageNum: 1, pageSize: 10, archiveId, orderBy: '', sort: '', }).then(res => { if(res.code !== 200){ message.error(res.message) }; }) this.setState({ file: {}, fileId: '', fileUploadState: null, fileplanNumber: 0, fileList, interFaceStart: 0, }); }) } else { this.setState({ fileUploadState: fail, }); } } } // 计算分片文件的MD5 getSliceFileMD5 = (blob) => { return new Promise((resolve, reject) => { const sliceFileReader = new FileReader(); let sliceFileMD5 = ''; sliceFileReader.onerror = reject; sliceFileReader.onload = (event) => { const spark = new SparkMD5.ArrayBuffer(); spark.append(event.target.result) sliceFileMD5 = spark.end(); // 返回分片MD5 resolve(sliceFileMD5); } sliceFileReader.readAsArrayBuffer(blob); }) } // 上传分片文件 uploadSliceFile = (fileMap, chunks) => { return new Promise((resolve, reject) => { const { sliceFileMD5, file, index } = fileMap; const fileBlob = new File([file], 'AAA.exe', { type: 'application/x-msdownload' }) const { fileId } = this.state; const formData = new FormData(); formData.append('fileId', fileId); formData.append('MD5', sliceFileMD5); formData.append('partSequence', index); formData.append('fileBlob', fileBlob); axios.post('', { formData, headers: { 'Content-Type': 'multipart/form-data', } }).then(res => { if(res.data.code !== 200){ message.error(res.data.message) return false; } const fileplanNumber = (index / chunks.length) * 100; this.setState({ fileplanNumber, }); resolve(true); }) }) } }6.1 代码解说:
6.1.1 使用
new关键字创建FileReader实例的主要原因包括:1. 对象实例化机制
FileReader是浏览器提供的构造函数,必须通过new调用才能生成可操作的对象实例。- 直接调用
FileReader()会抛出类型错误(如TypeError: Illegal constructor)。2. 异步操作隔离
- 每个
FileReader实例独立管理自己的文件读取状态和事件,避免多文件操作时的状态冲突。- 例如同时读取两个文件需要两个独立的实例
6.1.2 // 获取文件切片
this.getSliceFile()方法的核心逻辑:1. 文件分片生成
- 使用
File.slice()方法将大文件切割为固定大小的分片(如25MB/片),通过while循环控制分片区间计算- 分片起始位置(
start)和结束位置(end)需动态调整,确保最后一片正确处理剩余数据const piece = 25 * 1024 * 1024; // 分片大小 let start = 0; while (start < file.size) { const end = Math.min(start + piece, file.size); const chunk = file.slice(start, end); start = end; }2. 分片元数据管理
- 为每个分片生成唯一索引(
index)和MD5校验值(sliceFileMD5),用于服务端校验和断点续传- 分片信息存储到数组时需包含文件二进制数据、序号和哈希值
chunksList.push({ file: blob, index: currentIndex++, sliceFileMD5: await this.calculateMD5(blob) });3. 断点续传实现
- 根据
interFaceStart参数跳过已上传分片,仅处理未完成的分片- 上传进度需结合已跳过和当前上传分片数动态计算
const uploadChunks = chunksList.slice(interFaceStart);4. 发送合并请求、校验合并结果
7 部分代码2:
// 上传文件/文件夹组件 UploadFile = () => { const { catalogList } = this.props; // 上传文件 const uploadProps = { showUploadList: false, action: `${ctxApiPrefix}/archiveFile/uploadArchiveFile`, // 注意这里修正了action路径 method: 'post', data: { archiveId: catalogList.length > 0 ? catalogList[catalogList.length - 1].archiveId : 'root' }, onChange: this.handleChange, }; return ( <Menu> <Menu.Item key="0"> <Upload {...uploadProps}> <Button type="link" style={{ color: '#81a37f' }}>上传文件</Button> </Upload> </Menu.Item> <Menu.Item key="1"> <Upload beforeUpload={value => this.beforeUpload(value)}> <Button type="link" style={{ color: '#81a37f' }}>上传大文件</Button> </Upload> </Menu.Item> </Menu> ); }
更多推荐

所有评论(0)