公众号:程序员白特,关注我,每天进步一点点~
前端上传文件很大时,会出现各种问题,比如连接超时了,网断了,都会导致上传失败,这个时候就需要将文件切片上传,下面我们就来学习一下如何使用vue实现大文件切片上传吧
大文件为什么要切片上传
前端上传文件很大时,会出现各种问题,比如连接超时了,网断了,都会导致上传失败;
服务端限制了单次上传文件的大小;
项目实际场景
客户端需要上传一个算法包文件到服务器,这个算法包实测 3.7G
nginx配置文件 上传文件大小最大值为100M
,
切片上传原理
通过file.slice
将大文件chunks
切成许多个大小相等的chunk
将每个chunk
上传到服务器
服务端接收到许多个chunk
后,合并为chunks
第一版
先对文件按指定大小进行切片
/*** file: 需要切片的文件* chunkSize: 每片文件大小,1024*1024=1M*/
chunkSlice(file, chunkSize) {const chunks = [],size = file.size,total = Math.ceil(size / chunkSize)for (let i = 0; i < size; i += chunkSize) {chunks.push({total,blob: file.slice(i, i + chunkSize),})}return chunks
}
处理切片后的文件,后端想要我传给他一个json对象,所以使用readAsDataURL
读取文件
这里使用了一个插件spark-md5
来生成每个切片的MD5
async handleFile(chunks) {const res = []for (const item of chunks) {const { bytes, md5 } = await this.addMark(item.blob)item.blob = bytesitem.md5 = md5res.push(md5)}return res
},
// 使用FileReader读取每一片数据,并生成MD5编码
async addMark(chunk) {return new Promise((resolve, reject) => {const reader = new FileReader()const spark = new SparkMD5()reader.readAsDataURL(chunk)reader.onload = function (e) {const bytes = e.target.resultspark.append(bytes)const md5 = spark.end()resolve({ bytes, md5 })}})
},
组装数据,包括每一片的排列顺序index
,总共切了多少片total
,文件IDfileID
,每一片的md5编码md5
,每一片数据fileData
mergeData(chunks) {const fileId = this.getUUID()const data = []for (let i = 0; i < chunks.length; i++) {const obj = {fileId,fileData: chunks[i].blob,//每片切片的数据fileIndex: i + 1,//每片数据索引fileTotal: chunks[i].total + '',md5: chunks[i].md5,}data.push(obj)}return { data, fileId }
},
上传文件,这里使用并发上传文件,提升文件上传速度
const chunks = chunkSlice(file,1024*1024)
this.handleFile(chunks)
const data = this.mergeData(chunks)for(let i = 0; i < data.length; i++){this.uplload(data[i])
}
第一版遇到的问题
文件太大,切片太小,上传接口的timeout
太短,并发请求时,全都在pendding
,导致请求出错
第一版问题解决
对上传文件接口的timeout
修改,调整时长,大一点
限制每次并发的数量,我用的是500个每次
第二版,切片 + web worker
为什么要使用web worker
在生成文件MD5
编码时,需要读文件,是一个I/O
操作,会阻塞页面,文件太大,导致页面卡死
将耗时操作转移到worker
线程,主页面就不会卡住
vue2,使用worker
yarn add worker-loader
vue.config.js 配置
// vue.config.js
chainWebpack(config) {config.module.rule('worker').test(/\.worker\.js$/).use('worker-loader').loader('worker-loader')// .options({ inline: 'fallback' })// 这个配置是个坑,不要加
},
新建file.worker.js
// file.worker.js
import SparkMD5 from 'spark-md5'const chunkSlice = (file, chunkSize) => {const chunks = [],size = file.size,total = Math.ceil(size / chunkSize)for (let i = 0; i < size; i += chunkSize) {chunks.push({total,blob: file.slice(i, i + chunkSize),})}return chunks
}
const handleFile = async (chunks) => {const res = []for (const item of chunks) {const { bytes, md5 } = await addMark(item.blob)item.blob = bytesitem.md5 = md5res.push(md5)}return res
}
const addMark = (chunk) => {return new Promise((resolve, reject) => {const reader = new FileReader()const spark = new SparkMD5()reader.readAsDataURL(chunk)reader.onload = function (e) {const bytes = e.target.resultspark.append(bytes)const md5 = spark.end()resolve({ bytes, md5 })}})
}
const mergeData = (chunks, fileName, options) => {const fileId = getUUID() // 这里更好的方式是读整个文件的 MD5const data = []for (let i = 0; i < chunks.length; i++) {const obj = {...options,suffix: '.tar.gz',fileId,fileName,fileData: chunks[i].blob,fileIndex: i + 1 + '',fileTotal: chunks[i].total + '',md5: chunks[i].md5,}data.push(obj)}return { data, fileId }
}
const getUUID = () => {return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16))
}
const dataSlice = (data, step, fileId) => {const total = Math.ceil(data.length / step)let index = 1for (let i = 0; i < data.length; i += step) {const params = {type: 'workerFile',index,total,fileId,data: data.slice(i, i + step),}self.postMessage(params)index++}
}
self.addEventListener('error', (event) => {console.log('worker error', event)
})self.addEventListener('message', async (event) => {// 确保接受的是我想要的消息 if (!event.data.type) returnif (event.data.type != 'file') returnconsole.log('worker success', event)const { file, chunkSize } = event.dataconst chunks = chunkSlice(file, chunkSize)const allMD5 = await handleFile(chunks)console.log(allMD5)// 此处 allMD5 可用来做后续的断点续传const { data, fileId } = mergeData(chunks, file.name)// 这里对处理好的数据进行切片,分片传递给主线程,是由于 Web Worker 试图将大量数据复制到主线程中,会导致内存溢出。dataSlice(data, 100, fileId)})
这个报错一般是在使用 JavaScript Web Worker 时出现的,通常是由于 Web Worker 试图将大量数据复制到主线程中,导致内存溢出所引起的。
主进程使用
// xxx.vue文件
import Worker from '@/utils/worker/file.worker.js'const worker = new Worker()
worker.postMessage({ type: 'file', file: this.curFile, chunkSize: 1024 * 1024 })worker.onerror = (error) => {console.log('main error', error)worker.terminate()
}const finalData = []
worker.onmessage = async (event) => {console.log('main success', event)if (event.data.type != 'workerFile') returnconst fileId = mergeWorkerData(finalData, event.data)if (fileId) {worker.terminate()const status = await stepLoad(finalData, 500)if (!status) {this.$message.error('文件上传失败')} else {this.$message.success('文件上传成功')}}
}mergeWorkerData = (res, params) => {res.push(...params.data)return params.index == params.total ? params.fileId : false
}const stepLoad = async (data, step) => {const res = []for (let i = 0; i < data.length; i += step) {res.push(data.slice(i, i + step))}for (const item of res) {const chunkRes = await Promise.all(item.map((v) => this.$api.upload(v)))if (chunkRes.some((v) => v.httpCode != 0)) {return false}const isEnd = chunkRes.filter((v) => v.finish)if (isEnd.length) {return true}}
}
总结
worker
引入脚本或三方库可以使用importScript()
,但是我没弄成功,一使用importScript()
就会报错,Renference: importScript() xxxxxxxxxxxx
,如果你们弄出来了,或者知道为什么,可以在下面留言~