vue实现大文件切片上传的示例详解
作者:可乐加冰515
大文件为什么要切片上传
前端上传文件很大时,会出现各种问题,比如连接超时了,网断了,都会导致上传失败
服务端限制了单次上传文件的大小
项目实际场景
客户端需要上传一个算法包文件到服务器,这个算法包实测 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 = bytes item.md5 = md5 res.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.result spark.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 = bytes item.md5 = md5 res.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.result spark.append(bytes) const md5 = spark.end() resolve({ bytes, md5 }) } }) } const mergeData = (chunks, fileName, options) => { const fileId = getUUID() // 这里更好的方式是读整个文件的 MD5 const 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 = 1 for (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) return if (event.data.type != 'file') return console.log('worker success', event) const { file, chunkSize } = event.data const 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') return const 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
,如果你们弄出来了,或者知道为什么,可以在下面留言
以上就是vue实现大文件切片上传的示例详解的详细内容,更多关于vue大文件切片上传的资料请关注脚本之家其它相关文章!