目录
大文件上传 优点:
- 文件太大分片上传能加快上传速度,提高用户体验
- 能断点续传 如果上次上传失败或者中途离开的话下一次上传过的就不用重头开始了
- 已经上传过的文件根据HASH查询直接秒传
大文件上传缺点:
1.后台可能设置了请求时长限制,太久会上传失败(解决:后端不设置上传时长)
2.NGINX可能设置了文件上传的最大限制导致失败(解决:比如分片25M,nginx设置文件上传最大限度50M)
大文件上传原理:
- 用户选择要上传的大文件,计算整个文件的MD5。
- 前端根据分片大小将文件切分成多个小块,计算每个分片文件的MD5。
- 逐个上传每个小块到服务器端。
- 服务器端接收并保存每个小块。
- 在服务器端,根据上传的小块将它们合并成完整的文件。
为什么要用md5
因为每个文件都会有自己专属独立的md5值,就像是每个人的身份证,比如我们在某个平台发布视频,将视频文件二次上传的时候就会遇到不容易过审的原因,同一个MD5就有很大的机率显示搬运被退回。刚好后端同学也可以通过MD5这种特性来判断上传的文件是否完整。
如何快速计算文件的 md5 值呢? 我们使用 js-spark-md5 这个库
实现流程:
在upload组件上传文件的钩子函数beforeUpload() 中:
获取切片文件:设置切片文件大小、每次上传的开始字节,每次上传的结尾字节。文件切片的核心是使用Blob 对象的 slice 方法:
javascriptvar blob = file.slice([start [, end [, contentType]]]};
start 和 end 代表 Blob 里的下标,表示被拷贝进新的 Blob 的字节的起始位置和结束位置。contentType 会给新的 Blob 赋予一个新的文档类型,很少使用。
计算分片文件的MD5
上传分片文件,断点续传(如何实现断点续传,关键点是后端需要记录文件文件切片的信息。用户在上传一个文件之前,先询问服务器,当前文件是否存在已经上传完毕的切片,如果存在的话,需要返回切片信息。前端根据返回的信息,调整当前的进度,上传未完成的切片)
检验分片数量及上传的结果,全部上传,文件合并:
- 前端发送切片完成后,发送一个合并请求,后端收到请求后,将之前上传的切片文件合并。(上面展示的代码采用 这个)
- 后台记录切片文件上传数据,当后台检测到切片上传完成后,自动完成合并。
- 创建一个和源文件大小相同的文件,根据切片文件的起止位置直接将切片写入对应位置。
部分代码1:
javascriptexport default class Project extends React.PureComponent { construction (props){ this.state({ file: {}, fileplanNumber: 0, // 上传文件进度的百分比 fileUploadState: '', // 上传文件的状态,fail失败, success成功 interFaceStart: '', // 文件上传时的分片文件下标, 作用于断点续传 }) } beforeUpload = (file) => { this.setState({ file, // 把file存起来 }) let reader = new FileReader(); let md5 = ''; reader.onload = (event) => { const spark = new SparkMD5.ArrayBuffer; spark.append(e.target.value); md5 = spark.end(); axios.post('', { 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, // 文件上传时,分片文件下标,为了断点续传 }, () => { this.getSliceFile() // 获取文件切片 }) }) } reader.readArrayBufffer(file); 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.call(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); }) }) } }
部分代码2: