面试官:如何实现大文件切片上传?
公众号:程序员白特,关注我,每天进步一点点~
前端上传文件很大时,会出现各种问题,比如连接超时了,网断了,都会导致上传失败,这个时候就需要将文件切片上传,下面我们就来学习一下如何使用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 = 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
,如果你们弄出来了,或者知道为什么,可以在下面留言~