vue 多文件上传,vue文件批量上传
上传文件的常用方式可能是新建表单数据。追加文件后,将它们发布到后端。但是如果用这种方式上传大文件,很容易造成上传超时的问题。所以本文将使用Vue NodeJS上传大文件,有需要的可以参考。
目录
总体思路项目演示前端接口文件切片哈希计算查询切片状态切片上传(断点续传)文件合并的总体上传进度文件优化请求并发控制哈希值计算优化常见的文件上传方式可能是新的FormData,追加文件后上传到后端。但是如果用这种方式上传大文件,很容易造成上传超时的问题,一旦失败就要重新开始。在漫长的等待过程中,用户不能刷新浏览器,否则之前的所有努力都白费了。所以这类问题一般都是切片上传的。
整体思路
将一个文件切割成若干个小文件进行哈希计算,需要计算出一个文件的唯一标识,以便下次上传时筛选出剩余的切片。所有切片上传完毕后,通知服务器切片合成上传成功,整个过程中通知前端文件路径。如果整个过程失败,下次重新上传时,因为之前已经计算过文件hash,可以筛选出没有上传的切片(断点续传);如果整个文件已经上传,则不需要传输(第二次传输)。
项目演示
这里vue和node分别用来搭建前端和后端。
前端界面
fileUpload.vue
模板
div class=wrap
差异
El-上传
ref=file
:http-request=handleFileUpload
action=#
class=头像上传者
:show-file-list=false
El-button type=主上传文件/el-button
/El-上传
差异
计算div哈希的进度:/div
El-progress:stroke-width= 20 :text-inside= true :percentage= hash progress /El-progress
/div
差异
Div上传进度:/div
El-progress:stroke-width= 20 :text-inside= true :percentage= uploeadprogress /El-progress
/div
/div
/div
/模板
文件切片
可以使用file . prototype . slice file upload . vue的方法对文件进行切片
Const CHUNK_SIZE=1024*1024//每个切片为1M
从“spark-md5”导入sparkMD5
导出默认值{
名称:文件-上传,
data(){
返回{
File:null,//上传的文件
组块:[],//切片
HashProgress:0,//哈希值计算进度
哈希:“”
}
},
方法:{
异步handleFileUpload(e){
如果(!文件){
返回
}
this.file=file
this.upload()
},
//文件上传
异步上传(){
//切片
const chunks=this . create file chunk(this . file)
//.
//哈希计算
const hash=wait this . calculate hash 1(chunks)
}
},
//文件切片
create file CHUNK(SIZE=CHUNK _ SIZE){
const chunks=[];
设cur=0;
const maxLen=math . ceil(this . file . SIZE/CHUNK _ SIZE)
while(curmaxLen){
const start=cur * CHUNK _ SIZE
const end=((start CHUNK _ SIZE)=this . file . SIZE)?this . file . SIZE:start CHUNK _ SIZE;
chunks.push({index:cur,file:this.file.slice(start,end)})
坏蛋
}
返回块
},
}
hash计算
文件的唯一哈希值可以通过md5计算。
在这里,您可以使用spark-md5库来递增地计算文件的哈希值。
calculateHash1(块){
const spark=new sparkMD5。ArrayBuffer()
让计数=0
const len=chunks.length
让哈希
const self=this
常数开始时间=新日期()。getTime()
返回新承诺((解决)={
const loadNext=index={
const reader=new FileReader()
//逐片读取文件切片
读者。readasarraybuffer(chunks[index].文件)
reader.onload=函数(e){
常量结束时间=新日期()。getTime()
组块[计数]={.组块[计数],时间:结束时间-开始时间}
数数
//读取成功后利用发动做增量计算
火花.附加(目标结果)
if(count==len){
self.hashProgress=100
//返回整个文件的混杂
hash=spark.end()
解析(哈希)
}否则{
//更新混杂计算进度
self.hashProgress=100/len
loadNext(索引1)
}
}
}
loadNext(0)
})
},
可以看到整个过程还是比较费时间的,有可能会导致用户界面阻塞(卡),因此可以通过网状结构等手段优化这个过程,这点我们放在最后讨论
查询切片状态
在知道了文件的混杂值以后,在上传切片前我们还要去后端查询下文件的上传状态,如果已经上传过,那就没有必要再上传,如果只上传了一部分,那就上传还没有上过过的切片(断点续传)
前端fileUpload.vue
//.
方法:{
//.
异步上传(){
//.切片,计算混杂
this.hash=哈希
//查询是否上传将混杂和后缀作为参数传入
这个. http.post(/checkfile ,{
哈希,
ext:this.file.name.split( . ).流行()
})。然后(res={
//接口会返回两个值上传:布尔型表示整个文件是否上传过和上传列表哪些切片已经上传
const {uploaded,uploadedList}=res.data
//如果已经上传过,直接提示用户(秒传)
如果(已上传){
归还这个message.success(秒传成功)
}
//这里我们约定上传的每个切片名字都是哈希-索引
this.chunks=chunks.map((chunk,index)={
const name=hash -索引
const isChunkUploaded=(上传列表。包括(姓名))?真:假//当前切片是否有上传
返回{
哈希,
姓名,
索引,
chunk:chunk.file,
进度:isChunkUploaded?100:0//当前切片上传进度,如果有上传即为100 否则为0,这是用来之后计算总体上传进度
}
})
//上传切片
this.uploadChunks(uploadedList)
})
}
}
文件切片这。大块
服务端server/index.js
const Koa=require(koa )
const Router=require( KOA-Router )
const KOA body=require( KOA-body );
const path=require(path )
const fse=require(fs-extra )
常量应用程序=新树袋熊()
常量路由器=新路由器()
//文件存放在公众的下
常量UPLOAD _ DIR=path。resolve(_ _ dirname, public )
app.use(koaBody({
multipart:true,//支持文件上传
}));
router.post(/checkfile ,async (ctx)={
常数体=CTX。请求。身体;
const {ext,hash}=body
//合成后的文件路径文件名hash.ext
const文件路径=path。resolve(UPLOAD _ DIR,` ${hash}).${ext} `)
让上传=假
let uploadedList=[]
//判断文件是否已上传
if(fse.existsSync(filePath)){
上传=真
}否则{
//所有已经上传过的切片被存放在一个文件夹,名字就是该文件的混杂值
uploaded list=await get uploaded list(路径。resolve(上传目录,哈希))
}
ctx.body={
代码:0,
数据:{
已上传,
上传列表
}
}
})
异步函数getUploadedList(目录路径){
//将文件夹中的所有非隐藏文件读取并返回
返回fse.existsSync(dirPath)?(await fse.readdir(dirPath)).filter(name=name[0]!==.):[]
}
切片上传(断点续传)
再得知切片上传状态后,就能筛选出需要上传的切片来上传。前端fileUpload.vue
uploadChunks(uploadedList){
//每一个要上传的切片变成一个请求
常量请求=this。大块的。筛选器(块=!上传的列表。包括(大块。姓名))。映射((块,索引)={
const form=new FormData()
//所有上传的切片会被存放在一个文件夹,文件夹名字就是该文件的混杂值因此需要混杂和名字
form.append(chunk ,chunk.chunk)
form.append(hash ,chunk.hash)
form.append(name ,chunk.name)
//因为切片不一定是连续的,所以指数需要取矮胖的人或物对象中的指数
返回{表单,索引:chunk.index,错误:0}
})//所有切片一起并发上传。map(({form,index})={
归还这个http.post(/uploadfile ,form,{
onUploadProgress:进度={
this.chunks[index].进度=数字(((进步。已加载/进度。总计)* 100).toFixed(2)) //当前切片上传的进度
}
})
})
无极. all(请求)。然后((res)={
//所有请求都成功后发送请求给服务端合并文件
this.mergeFile()
})
},
服务端
router.post(/uploadfile ,async (ctx)={
const body=ctx.request.body
常量文件=CTX。请求。文件。矮胖的人或物
const {hash,name }=正文
//切片存放的文件夹所在路径
const块路径=路径。解析(上传目录,哈希)
如果(!fse.existsSync(chunkPath)){
await fse.mkdir(chunkPath)
}
//将文件从临时路径里移动到文件夹下
await fse.move(file.filepath,` ${chunkPath}/${name} `)
ctx.body={
代码:0,
消息:`消息切片上传成功`
}
})
上传后切片保存的位置
文件总体上传进度
总体上传进度取决于每个切片上传的进度和文件总体大小,可以通过计算属性来实现
fileUpload.vue
uploaedProgress(){
如果(!this.file !this.chunks.length){
返回0
}
//累加每个切片已上传的部分
const loaded=this。大块的。映射(区块={
const size=chunk.chunk.size
const chunk _ loaded=chunk。进度/100 *尺寸
返回区块已加载
}).减少((acc,cur)=acc cur,0)
返回parse int((loaded * 100)/this。文件。尺寸).toFixed(2))
},
合并文件
前端fileUpload.vue
//要传给服务端文件后缀,切片的大小和混杂值
mergeFile(){
这个. http.post(/mergeFile ,{
ext:this.file.name.split( . ).pop(),
大小:CHUNK_SIZE
哈希:this.hash
}).然后(res={
if(资源资源数据){
console.log(res.data)
}
})
},
服务端
router.post(/mergeFile ,async (ctx)={
const body=ctx.request.body
const {ext,size,hash}=body
//文件最终路径
const文件路径=path。resolve(UPLOAD _ DIR,` ${hash}).${ext} `)
等待合并文件(文件路径,大小,哈希)
ctx.body={
代码:0,
数据:{
url:`/public/${hash} .${ext} `
}
}
})
异步函数合并文件(文件路径,大小,哈希){
//保存切片的文件夹地址
常量块目录=路径。解析(上传目录,哈希)
//读取切片
让chunks=await fse。读取目录(块目录)
//切片要按顺序合并,因此需要做个排序
chunks=chunks.sort((a,b)=a . split(-)[1]-b . split(-)[1])
//切片的绝对路径
组块=组块。map(cpath=path。解析(块目录,路径))
等待合并块(块,文件路径,大小)
}
//边读边写至文件最终路径
函数合并块(文件,目标,块大小){
常量管道流=(文件路径,写流)={
返回新承诺((解决,拒绝)={
const读取流=fse。创建读取流(文件路径)
readStream.on(end ,()={
//每一个切片读取完毕后就将其删除
fse.unlinkSync(文件路径)
解决()
})
readStream.pipe(writeStream)
})
}
const pipes=files.map((文件,索引)={
返回管道流(file,fse.createWriteStream(dest,{
开始:索引*块大小
结束:(索引1)*区块大小
}))
});
返回Promise.all(管道)
}
大文件切片上传的功能已经实现,让我们来看下效果(这里顺便展示一下单个切片的上传进度)
可以看出,由于大量切片请求并发上传,虽然浏览器本身对并发请求数量有限制(可以看出很多请求处于挂起状态),但还是造成了卡顿,所以这个过程还是需要优化。
优化
请求并发数控制
fileUpload.vue
逐片上传
这也是最直接的方法,可以看作是并发请求的另一个极端。如果一个上传成功,将会上传第二个。这里,我们必须处理错误重试。如果连续三次失败,整个上传过程将被终止。
uploadChunks(uploadedList){
console.log(this.chunks)
const requests=this . chunks . filter(chunk=!uploaded list . includes(chunk . name))。映射((块,索引)={
const form=new FormData()
form.append(chunk ,chunk.chunk)
form.append(hash ,chunk.hash)
form.append(name ,chunk.name)
返回{form,index:chunk.index,error:0}
})
//.map(({form,index})={
//返回这个。$http.post(/uploadfile ,form,{
//onuploadpress:progress={
//this.chunks[index]。进度=数字(((progress . loaded/progress . total)* 100)。toFixed(2))
//}
//})
//})
////console.log(请求)
//Promise.all(请求)。然后((res)={
//console.log(res)
//this.mergeFile()
//})
const sendRequest=()={
返回新承诺((解决,拒绝)={
const upLoadReq=(i)={
const req=请求数[i]
const {form,index}=req
这个。$http.post(/uploadfile ,form,{
onUploadProgress:进度={
this.chunks[index]。进度=数字(((progress . loaded/progress . total)* 100)。toFixed(2))
}
})。然后(res={
//最后一块上传成功,整个过程完成。
if(i==requests.length-1){
解决()
返回
}
上传请求(i 1)
})。catch(错误={
this.chunks[index]。进度=-1
if(req.error3){
请求错误
//错误累积后重试
上传请求(一)
}否则{
拒绝()
}
})
}
上传请求(0)
})
}
//整个过程成功后合并文件
sendRequest()。然后(()={
this.mergeFile()
})
},
您可以看到一次只有一个上传请求
结果文件
多个请求并发
一个个请求确实可以解决停滞问题,但是效率有点低。我们还可以在此基础上实现有限数量的并发。
一般这类问题的思路是形成一个任务队列。开始时,从请求中取出指定数量的并发请求(假设是三个),填充队列并分别开始请求任务。每个任务完成后,关闭任务并退出队列,然后从请求理论中取出一个元素,加入队列并执行,直到请求为空。在这里,如果某个请求失败,应该再次将其填充到请求队列的头部,以便下次执行时可以重试该请求。
异步上传块(uploadedList){
console.log(this.chunks)
const requests=this . chunks . filter(chunk=!uploaded list . includes(chunk . name))。映射((块,索引)={
const form=new FormData()
form.append(chunk ,chunk.chunk)
form.append(hash ,chunk.hash)
form.append(name ,chunk.name)
返回{form,index:chunk.index,error:0}
})
const sendRequest=(limit=1,task=[])={
let=0//用于记录成功请求的数量。当它等于len-1时,所有切片都已成功上传。
让stop=false//标记错误情况。如果某个片段中的错误数大于3,则整个任务标记失败,其他并发请求不会递归执行。
const len=requests .长度
返回新承诺((解决,拒绝)={
const upLoadReq=()={
如果(停止){
返回
}
const req=requests.shift()
如果(!req){
返回
}
const {form,index}=req
这个http.post(/uploadfile ,form,{
onUploadProgress:进度={
this.chunks[index].进度=数字(((进步。已加载/进度。总计)* 100).toFixed(2))
}
})。然后(res={
//最后一片
if(count==len-1){
解决()
}否则{
数数
上传请求()
}
})。接住(错误={
this.chunks[index].进度=-1
if(req.error3){
请求错误
requests.unshift(req)
上传请求()
}否则{
isStop=true
拒绝()
}
})
}
while(limit0){
//模拟形成了一个队列,每次结束再递归执行下一个任务
上传请求()
限制-
}
})
}
发送请求(3).然后(res={
console.log(res)
this.mergeFile()
})
},
hash值计算优化
除了请求并发需要控制意外,哈希值的计算也需要关注,虽然我们采用了增量计算的方法,但是可以看出依旧比较费时,也有可能会阻塞用户界面
webWork
这相当于多开了一个线程,让混杂计算在新的线程中计算,然后将结果通知会主线程
计算哈希工作(块){
返回新承诺((解决)={
//这个射流研究…得独立于项目之外
this.worker=新工人(/hash.js )
//切片传入现成
这个。工人。后期消息({ chunks })
this.worker.onmessage=e={
//线程中返回的进度和混杂值
const {progress,hash}=e.data
这个。哈希进度=数量(进度。到固定(2))
如果(哈希){
解析(哈希)
}
}
})
},
hash.js
//独立于项目之外,得单独
//引入火花讯息摘要5
自我。导入脚本( spark-MD5。量滴js’)
self.onmessage=e={
//接受主线程传递的数据,开始计算
const {chunks }=电子数据
恒定火花=新的自己10 . spark MD5。ArrayBuffer()
让进度=0
让计数=0
const loadNext=index={
const reader=new FileReader()
读者。readasarraybuffer(chunks[index].文件)
reader.onload=e={
数数
火花.附加(目标结果)
if(count==chunks.length){
//向主线程返回进度和混杂
self.postMessage({
进步:100,
hash:spark.end()
})
}否则{
进度=100/组块。长度
//向主线程返回进度
self.postMessage({
进步
})
loadNext(计数)
}
}
}
loadNext(0)
}
时间切片
还有一种做法就是借鉴反应纤维架构,可以通过时间切片的方式在浏览器空闲的时候计算混杂值,这样浏览器的渲染是联系的,就不会出现明显卡顿
计算哈希德尔(块){
返回新承诺(resolve={
const spark=new sparkMD5 .ArrayBuffer()
让计数=0
const appendToSpark=async file={
返回新承诺(resolve={
const reader=new FileReader()
reader.readAsArrayBuffer(文件)
reader.onload=e={
火花.附加(目标结果)
解决()
}
})
}
const workLoop=async deadline={
//当切片没有读完并且浏览器有剩余时间
而(数组块。长度截止时间。剩余时间()1){
等待appendToSpark(块[计数])。文件)
数数
if(countchunks.length){
这个。哈希进度=数量(((100 *计数)/组块。长度).toFixed(2))
}否则{
this.hashProgress=100
const hash=spark.end()
解析(哈希)
}
}
window.requestIdleCallback(工作循环)
}
window.requestIdleCallback(工作循环)
})
}
以上是Vue NodeJS上传大文件的样例代码的细节。更多关于Vue NodeJS上传大文件的信息,请关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。