JS上传大文件的三种解决方案
原理
js将大文件分成多分,全部上传成功之后,调用合并接口合成文件。如果传输中断,下次上传的时候过滤掉已经上传成功的分片,将剩余的分片上传,成功之后合并文件。
前置条件
-
获取uoloadId接口(用于标记分片)
-
分片上传接口
-
合成文件接口(后端自动合成则不需要)
-
查询已上传的分片列表接口(断点续传)
文件分片
h5中使用slice就可以分割文件
// 文件分片大小const maxFileSize = 100 * 1024// 分片数量const nums = Math.ceil(file.size / maxFileSize)for (let i = 1; i <= nums; i++) { // 文件上传进度 let progress = 0 // 分割文件 上传 upload(file.slice((i - 1) * maxFileSize, i * maxFileSize), uploadId).then(() => { progress++ if (progress >= nums) { // 完全上传成功 // 合并文件 merge(uploadId).then(res => { // 返回文件地址 console.log(res) }) } })}
断点续传
在上传分片的时候我们将文件和uploadId存入本地,下次上传时用uploadId从接口获取已经上传成功的分片(这里没有接口,我们存入本地),将已上传的分片过滤,上传剩余。
// 使用文件的属性拼成一个唯一id,作为localStorage的keyconst id = file.lastModified + file.size + file.type + file.name// 取uploadId 和 已上传成功的分片let { ids = [], uploadId } = Storage.getObj(id)// 进度let progress = ids.length// 分片数量const nums = Math.ceil(file.size / maxFileSize)for (let i = 1; i <= nums; i++) { // 如果当前分片已经上传成功 跳过本次上传 if (ids.includes(i)) continue // 分割文件 上传 upload(file.slice((i - 1) * maxFileSize, i * maxFileSize), uploadId).then(() => { progress++ if (progress >= nums) { // 完全上传成功 // 合并文件 merge(uploadId).then(res => { // 返回文件地址 console.log(res) }) // 合并请求之后清除缓存 Storage.remove(id) } else { ids.push(i) // 已上传的分片存入本地 Storage.setObj(id, { ids, uploadId }) } })}
完整代码
class Uploader { // 分片大小(kb) chunkSize = 0 uuid = '' constructor({ chunkSize, upload, mergeFile, getUploadId }) { this.uuid = uuidv4() this.chunkSize = chunkSize * 1024 this.upload = upload // 上传文件 this.mergeFile = mergeFile // 合并文件 this.getUploadId = getUploadId // 获取uploadId } async start(file, onProgress) { // 判断是否使用分片上传 if (this.chunkSize > 0 && file.size > this.chunkSize) { // uuid拼接文件各个属性, 作为localStorage的key const key = this.uuid + file.lastModified + file.size + file.type + file.name // 去缓存中已上传的分片和uploadId let { chunks = [], uploadId } = Storage.getObj(key) // 从接口去uploadId if (!uploadId) { uploadId = await this.getUploadId() } return new Promise(resolve => { const chunkNums = Math.ceil(file.size / chunkSize) // 进度 let progress = 0 for (let i = 0; i < chunkNums; i++) { // 缓存中存在已上传的分片 跳过上传 if (chunks.includes(e => e.index === i)) continue this.upload( file.slice(i * chunkSize, (i + 1) * chunkSize), uploadId, i ).then(async res => { chunks.push({ index: i, chunk: file.slice(i * chunkSize, (i + 1) * chunkSize), uploadData: res.data }) progress++ onProgress(progress / chunkNums) if (progress >= chunkNums) { // 全部分片上传成功 Storage.remove(key) if (this.mergeFile) { resolve(await this.mergeFile(chunks, uploadId)) } resolve(res) } else { Storage.setObj(key, { chunks, uploadId }) } }) } }) } else { return this.upload(file) } }}// 实例化const uploader = new Uploader({ chunkSize: 100, getUploadId: GetUploadId, upload: (file, uploadId, index) => UploadFile(file, uploadId, index && index + 1), mergeFile: (chunks, uploadId) => MergeFile( uploadId, chunks.map(e => ({ partNumber: e.index + 1 })) )})// 上传图片调用async change(e) { console.log(e.target.files) if (!e.target.files.length) return const { data } = await uploader.start( e.target.files[0], e => (this.progress = e) ) console.log(readUrl + data.data)}
大文件下载
同理,当需要下载大文件时,可以将文件拆分(后端支持),下载完成后在前端拼接成一整个文件。
const chunks = []const nums = Math.ceil(file.size / maxFileSize)for (let i = 1; i <= nums; i++) { chunks.push(file.slice((i - 1) * maxFileSize, i * maxFileSize))}const blob = new Blob(chunks)
控件源码下载: