欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue+NodeJS實(shí)現(xiàn)大文件上傳的示例代碼

 更新時間:2022年05月18日 16:24:06   作者:YoYo君  
常見的文件上傳方式可能就是new一個FormData,把文件append進(jìn)去以后post給后端就可以了。但如果采用這種方式來上傳大文件就很容易產(chǎn)生上傳超時的問題。所以本文將利用Vue+NodeJS實(shí)現(xiàn)大文件上傳,需要的可以參考一下

常見的文件上傳方式可能就是new一個FormData,把文件append進(jìn)去以后post給后端就可以了。但如果采用這種方式來上傳大文件就很容易產(chǎn)生上傳超時的問題,而且一旦失敗還得從新開始,在漫長的等待過程中用戶還不能刷新瀏覽器,不然前功盡棄。因此這類問題一般都是通過切片上傳。

整體思路

  • 將文件切成多個小文件
  • hash計算,需要計算一個文件的唯一標(biāo)識,這樣下次再傳,就能篩選出剩余的切片進(jìn)行上傳。
  • 所有切片上傳后,通知服務(wù)端進(jìn)行切片合成
  • 上傳成功通知前端文件路徑
  • 整個過程如果出現(xiàn)失敗,下次再傳時,由于之前計算過文件hash,可以篩選出未傳數(shù)的切片續(xù)傳(斷點(diǎn)續(xù)傳); 如果整個文件已經(jīng)上傳過,就不需要傳輸(秒傳)

項(xiàng)目演示

這里用vue和node分別搭建前端和后端

前端界面

fileUpload.vue

<template>
    <div class="wrap">
        <div >
           <el-upload
             ref="file"
             :http-request="handleFileUpload"
             action="#"
             class="avatar-uploader"
             :show-file-list='false'
           >
               <el-button type="primary">上傳文件</el-button>
           </el-upload>
           <div>
               <div>計算hash的進(jìn)度:</div>
              <el-progress :stroke-width="20" :text-inside="true" :percentage="hashProgress"></el-progress> 
           </div>
           <div>
               <div>上傳進(jìn)度:</div>
              <el-progress :stroke-width="20" :text-inside="true" :percentage="uploaedProgress"></el-progress> 
           </div>
        </div>
    </div>
</template>

文件切片

利用 File.prototype.slice 的方法可以對文件進(jìn)行切片 fileUpload.vue

    const CHUNK_SIZE=1024*1024//每個切片為1M
    import sparkMD5 from 'spark-md5'
    export default {
        name:'file-upload',
        data(){
            return {
              file:null,//上傳的文件
              chunks:[],//切片
              hashProgress:0,//hash值計算進(jìn)度
              hash:''
            }
        },
        methods:{
            async handleFileUpload(e){
              if(!file){
                  return 
              }
              this.file=file
              this.upload()
            },
            //文件上傳
            async upload(){
             //切片
             const chunks=this.createFileChunk(this.file)
            //...
            //hash計算
              const hash=await this.calculateHash1(chunks)
            }
           },
            //文件切片
             createFileChunk(size=CHUNK_SIZE){
              const chunks=[];
              let cur=0;
              const maxLen=Math.ceil(this.file.size/CHUNK_SIZE)
              while(cur<maxLen){
                 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)})
                 cur++
              }
              return chunks
            },
        
    }

hash計算

利用md5可以計算出文件唯一的hash值

這里可以使用 spark-md5 這個庫可以增量計算文件的hash值

calculateHash1(chunks){
   const spark=new sparkMD5.ArrayBuffer()
   let count =0
   const len=chunks.length
   let hash
   const self=this
   const startTime = new Date().getTime()
   return new Promise((resolve)=>{
        const loadNext=index=>{
            const reader=new FileReader()
            //逐片讀取文件切片
            reader.readAsArrayBuffer(chunks[index].file)
            reader.onload=function(e){
                const endTime=new Date().getTime()
                chunks[count]={...chunks[count],time:endTime-startTime}
                count++
                //讀取成功后利用spark做增量計算
                spark.append(e.target.result)
                if(count==len){
                    self.hashProgress=100
                    //返回整個文件的hash
                     hash=spark.end()
                     resolve(hash)
                }else{
                    //更新hash計算進(jìn)度
                    self.hashProgress+=100/len
                    loadNext(index+1)
                }
            }
        }
        loadNext(0)
   })

},

可以看到整個過程還是比較費(fèi)時間的,有可能會導(dǎo)致UI阻塞(卡),因此可以通過webwork等手段優(yōu)化這個過程,這點(diǎn)我們放在最后討論

查詢切片狀態(tài)

在知道了文件的hash值以后,在上傳切片前我們還要去后端查詢下文件的上傳狀態(tài),如果已經(jīng)上傳過,那就沒有必要再上傳,如果只上傳了一部分,那就上傳還沒有上過過的切片(斷點(diǎn)續(xù)傳)

前端 fileUpload.vue

//...
methods:{
//...
async upload(){
//...切片,計算hash
this.hash=hash
//查詢是否上傳 將hash和后綴作為參數(shù)傳入
this.$http.post('/checkfile',{
  hash,
  ext:this.file.name.split('.').pop()
})
.then(res=>{
  //接口會返回兩個值 uploaded:Boolean 表示整個文件是否上傳過 和 uploadedList 哪些切片已經(jīng)上傳
   const {uploaded,uploadedList}=res.data
   //如果已經(jīng)上傳過,直接提示用戶(秒傳)
    if(uploaded){
        return  this.$message.success('秒傳成功')
    }
  //這里我們約定上傳的每個切片名字都是 hash+‘-'+index
    this.chunks=chunks.map((chunk,index)=>{
        const name=hash+'-'+index
        const isChunkUploaded=(uploadedList.includes(name))?true:false//當(dāng)前切片是否有上傳
        return {
            hash,
            name,
            index,
            chunk:chunk.file,
            progress:isChunkUploaded?100:0//當(dāng)前切片上傳進(jìn)度,如果有上傳即為100 否則為0,這是用來之后計算總體上傳進(jìn)度
        }
    })
    //上傳切片
    this.uploadChunks(uploadedList)
  })
 }
 
}

文件切片 this.chunks

服務(wù)端 server/index.js

const Koa=require('koa')
const Router=require('koa-router')
const koaBody = require('koa-body');
const path=require('path')
const fse=require('fs-extra')
const app=new Koa()
const router=new Router()
//文件存放在public下
const UPLOAD_DIR=path.resolve(__dirname,'public')
app.use(koaBody({
    multipart:true, // 支持文件上傳
}));


router.post('/checkfile',async (ctx)=>{
    const body=ctx.request.body;
    const {ext,hash}=body
    //合成后的文件路徑 文件名 hash.ext
    const filePath=path.resolve(UPLOAD_DIR,`${hash}.${ext}`)
    let uploaded=false
    let uploadedList=[]
    //判斷文件是否已上傳
    if(fse.existsSync(filePath)){
      uploaded=true
    }else{
    //所有已經(jīng)上傳過的切片被存放在 一個文件夾,名字就是該文件的hash值
      uploadedList=await getUploadedList(path.resolve(UPLOAD_DIR,hash))
    }
    ctx.body={
      code:0,
      data:{
        uploaded,
        uploadedList
      }
    }
})

async function getUploadedList(dirPath){
//將文件夾中的所有非隱藏文件讀取并返回
   return fse.existsSync(dirPath)?(await fse.readdir(dirPath)).filter(name=>name[0]!=='.'):[]
}

切片上傳(斷點(diǎn)續(xù)傳)

再得知切片上傳狀態(tài)后,就能篩選出需要上傳的切片來上傳。 前端 fileUpload.vue

uploadChunks(uploadedList){
 //每一個要上傳的切片變成一個請求
  const requests=this.chunks.filter(chunk=>!uploadedList.includes(chunk.name))
  .map((chunk,index)=>{
     const form=new FormData()
     //所有上傳的切片會被存放在 一個文件夾,文件夾名字就是該文件的hash值 因此需要hash和name
     form.append('chunk',chunk.chunk)
     form.append('hash',chunk.hash)
     form.append('name',chunk.name)
     //因?yàn)榍衅灰欢ㄊ沁B續(xù)的,所以index需要取chunk對象中的index
     return {form,index:chunk.index,error:0}
  })//所有切片一起并發(fā)上傳
  .map(({form,index})=>{
      return this.$http.post('/uploadfile',form,{
          onUploadProgress:progress=>{
              this.chunks[index].progress=Number(((progress.loaded/progress.total)*100).toFixed(2)) //當(dāng)前切片上傳的進(jìn)度
          }
      })
  })

  Promise.all(requests).then((res)=>{
   //所有請求都成功后發(fā)送請求給服務(wù)端合并文件
    this.mergeFile()
  })

},

服務(wù)端

router.post('/uploadfile',async (ctx)=>{
  const body=ctx.request.body
  const file=ctx.request.files.chunk
  const {hash,name}=body
 //切片存放的文件夾所在路徑
  const chunkPath=path.resolve(UPLOAD_DIR,hash)
  if(!fse.existsSync(chunkPath)){
      await fse.mkdir(chunkPath)
  }
 //將文件從臨時路徑里移動到文件夾下
  await fse.move(file.filepath,`${chunkPath}/${name}`)
  

  ctx.body={
    code:0,
    message:`切片上傳成功`
   } 

})

上傳后切片保存的位置

文件總體上傳進(jìn)度

總體上傳進(jìn)度取決于每個切片上傳的進(jìn)度和文件總體大小,可以通過計算屬性來實(shí)現(xiàn)

fileUpload.vue

uploaedProgress(){
    if(!this.file || !this.chunks.length){
        return 0
    }
    //累加每個切片已上傳的部分
   const loaded =this.chunks.map(chunk=>{
       const size=chunk.chunk.size
       const chunk_loaded=chunk.progress/100*size
       return chunk_loaded
    }).reduce((acc,cur)=>acc+cur,0)

   return parseInt(((loaded*100)/this.file.size).toFixed(2))
},

合并文件

前端 fileUpload.vue

//要傳給服務(wù)端文件后綴,切片的大小和hash值
mergeFile(){
    this.$http.post('/mergeFile',{
        ext:this.file.name.split('.').pop(),
        size:CHUNK_SIZE,
        hash:this.hash
    }).then(res=>{
        if(res && res.data){
            console.log(res.data)
        }
    })
},

服務(wù)端

router.post('/mergeFile',async (ctx)=>{
  const body=ctx.request.body
  const {ext,size,hash}=body
  //文件最終路徑
  const filePath=path.resolve(UPLOAD_DIR,`${hash}.${ext}`)
  await mergeFile(filePath,size,hash)
  ctx.body={
     code:0,
     data:{
         url:`/public/${hash}.${ext}`
     }
  }
})

async function mergeFile(filePath,size,hash){
  //保存切片的文件夾地址
  const chunkDir=path.resolve(UPLOAD_DIR,hash)
  //讀取切片
  let chunks=await fse.readdir(chunkDir)
  //切片要按順序合并,因此需要做個排序
  chunks=chunks.sort((a,b)=>a.split('-')[1]-b.split('-')[1])
  //切片的絕對路徑
  chunks=chunks.map(cpath=>path.resolve(chunkDir,cpath))
  await mergeChunks(chunks,filePath,size)
}

//邊讀邊寫至文件最終路徑
function mergeChunks(files,dest,CHUNK_SIZE){
  const pipeStream=(filePath,writeStream)=>{
    return new Promise((resolve,reject)=>{
        const readStream=fse.createReadStream(filePath)
        readStream.on('end',()=>{
            //每一個切片讀取完畢后就將其刪除
            fse.unlinkSync(filePath)
            resolve()
        })
        readStream.pipe(writeStream)
    })

  }

  const pipes=files.map((file,index) => {
  return pipeStream(file,fse.createWriteStream(dest,{
        start:index*CHUNK_SIZE,
        end:(index+1)*CHUNK_SIZE
    }))
  });
  return Promise.all(pipes)

}

大文件切片上傳的功能已經(jīng)實(shí)現(xiàn),讓我們來看下效果(這里順便展示一下單個切片的上傳進(jìn)度)

可以看到由于大量的切片請求并發(fā)上傳,雖然瀏覽器本身對同時并發(fā)的請求數(shù)有所限制(可以看到許多請求是pending狀態(tài)),但還是造成了卡頓,因此這個流程還是需要做一個優(yōu)化

優(yōu)化

請求并發(fā)數(shù)控制

fileUpload.vue

逐片上傳

這也是最直接的一種做法,可以看作是并發(fā)請求的另一個極端,上傳成功一個再上傳第二個,這里還要處理一下錯誤重試,如果連續(xù)失敗3次,整個上傳過程終止

uploadChunks(uploadedList){
  console.log(this.chunks)
  const requests=this.chunks.filter(chunk=>!uploadedList.includes(chunk.name))
  .map((chunk,index)=>{
     const form=new FormData()
     form.append('chunk',chunk.chunk)
     form.append('hash',chunk.hash)
     form.append('name',chunk.name)
     return {form,index:chunk.index,error:0}
  })
//   .map(({form,index})=>{
//       return this.$http.post('/uploadfile',form,{
//           onUploadProgress:progress=>{
//               this.chunks[index].progress=Number(((progress.loaded/progress.total)*100).toFixed(2))
//           }
//       })
//   })
// //   console.log(requests)
//   Promise.all(requests).then((res)=>{
//     console.log(res)
//     this.mergeFile()
//   })

  const sendRequest=()=>{
      return new Promise((resolve,reject)=>{
            const upLoadReq=(i)=>{
                const req=requests[i]
                const {form,index}=req
                this.$http.post('/uploadfile',form,{
                    onUploadProgress:progress=>{
                        this.chunks[index].progress=Number(((progress.loaded/progress.total)*100).toFixed(2))
                    }
                })
                .then(res=>{
                    //最后一片上傳成功,整個過程完成
                    if(i==requests.length-1){
                        resolve()
                        return
                    }
                    upLoadReq(i+1)
                })
                .catch(err=>{
                    this.chunks[index].progress=-1
                    if(req.error<3){
                        req.error++
                        //錯誤累加后重試
                        upLoadReq(i)
                    }else{
                        reject()
                    }
                })
           }
           upLoadReq(0)

      })


  }
  //整個過程成功后再合并文件
  sendRequest()
  .then(()=>{
     this.mergeFile()
  })

},

可以看到每次只有一個上傳請求

最終生成的文件

多個請求并發(fā)

逐個請求的確是可以解決卡頓的問題,但是效率有點(diǎn)低,我們還可以在這個基礎(chǔ)上做到有限個數(shù)的并發(fā)

一般這種問題的思路就是要形成一個任務(wù)隊列,開始的時候先從requests中取出指定并發(fā)數(shù)的請求對象(假設(shè)是3個)塞滿隊列并各自開始請求任務(wù),每一個任務(wù)結(jié)束后將該任務(wù)關(guān)閉退出隊列然后再從request說中取出一個元素加入隊列并執(zhí)行,直到requests清空,這里如果某一片請求失敗的話那還要再塞入request隊首,這樣下次執(zhí)行時還能從這個請求開始達(dá)到了重試的目的

async uploadChunks(uploadedList){
  console.log(this.chunks)
  const requests=this.chunks.filter(chunk=>!uploadedList.includes(chunk.name))
  .map((chunk,index)=>{
     const form=new FormData()
     form.append('chunk',chunk.chunk)
     form.append('hash',chunk.hash)
     form.append('name',chunk.name)
     return {form,index:chunk.index,error:0}
  })


const sendRequest=(limit=1,task=[])=>{
    let count=0 //用于記錄請求成功次數(shù)當(dāng)其等于len-1時所有切片都已上傳成功
    let isStop=false //標(biāo)記錯誤情況,如果某一片錯誤數(shù)大于3整個任務(wù)標(biāo)記失敗 并且其他并發(fā)的請求憑次標(biāo)記也不在遞歸執(zhí)行
    const len=requests.length
    return new Promise((resolve,reject)=>{
            const upLoadReq=()=>{
                if(isStop){
                    return
                }
                const req=requests.shift()
                if(!req){
                    return
                }
                const {form,index}=req
                this.$http.post('/uploadfile',form,{
                    onUploadProgress:progress=>{
                        this.chunks[index].progress=Number(((progress.loaded/progress.total)*100).toFixed(2))
                    }
                })
                .then(res=>{
                    //最后一片
                    if(count==len-1){
                    resolve()
                    }else{
                    count++
                    upLoadReq()
                    }
                })
                .catch(err=>{
                    this.chunks[index].progress=-1
                    if(req.error<3){
                        req.error++
                        requests.unshift(req)
                        upLoadReq()
                    }else{
                        isStop=true
                        reject()
                    }
                })
            }

            while(limit>0){
              //模擬形成了一個隊列,每次結(jié)束再遞歸執(zhí)行下一個任務(wù)
              upLoadReq()
              limit--
            }
    })

}

   sendRequest(3).then(res=>{
      console.log(res)
      this.mergeFile()
   })

},

hash值計算優(yōu)化

除了請求并發(fā)需要控制意外,hash值的計算也需要關(guān)注,雖然我們采用了增量計算的方法,但是可以看出依舊比較費(fèi)時,也有可能會阻塞UI

webWork

這相當(dāng)于多開了一個線程,讓hash計算在新的線程中計算,然后將結(jié)果通知會主線程

calculateHashWork(chunks){
   return new Promise((resolve)=>{
      //這個js得獨(dú)立于項(xiàng)目之外
      this.worker=new worker('/hash.js')
      //切片傳入現(xiàn)成
      this.worker.postMessage({chunks})
      this.worker.onmessage=e=>{
      //線程中返回的進(jìn)度和hash值
       const {progress,hash}=e.data
       this.hashProgress=Number(progress.toFixed(2))
       if(hash){
        resolve(hash)
       
       }
       
     }
   })
},

hash.js

//獨(dú)立于項(xiàng)目之外,得單獨(dú)

// 引入spark-md5

self.importScripts('spark-md5.min.js')


self.onmessage = e=>{
  // 接受主線程傳遞的數(shù)據(jù),開始計算
  const {chunks } = e.data
  const spark = new self.SparkMD5.ArrayBuffer()

  let progress = 0
  let count = 0

  const loadNext = index=>{
    const reader = new FileReader()
    reader.readAsArrayBuffer(chunks[index].file)
    reader.onload = e=>{
      count ++
      spark.append(e.target.result)

      if(count==chunks.length){
      //向主線程返回進(jìn)度和hash
        self.postMessage({
          progress:100,
          hash:spark.end()
        })
      }else{
        progress += 100/chunks.length
        //向主線程返回進(jìn)度
        self.postMessage({
          progress
        })
        loadNext(count)
      }
    }
  }
  loadNext(0)
}

時間切片

還有一種做法就是借鑒react fiber架構(gòu),可以通過時間切片的方式在瀏覽器空閑的時候計算hash值,這樣瀏覽器的渲染是聯(lián)系的,就不會出現(xiàn)明顯卡頓

calculateHashIdle(chunks){
    return new Promise(resolve=>{
        const spark=new sparkMD5.ArrayBuffer()
        let count=0
        const appendToSpark=async file=>{
            return new Promise(resolve=>{
                const reader=new FileReader()
                reader.readAsArrayBuffer(file)
                reader.onload=e=>{
                    spark.append(e.target.result)
                    resolve()
                }
            })
        }

        const workLoop=async deadline=>{
        //當(dāng)切片沒有讀完并且瀏覽器有剩余時間
            while(count<chunks.length && deadline.timeRemaining()>1){
                await appendToSpark(chunks[count].file)
                count++
                if(count<chunks.length){
                    this.hashProgress=Number(((100*count)/chunks.length).toFixed(2))
                }else{
                    this.hashProgress=100
                    const hash=spark.end()
                    resolve(hash)
                }
            }
            window.requestIdleCallback(workLoop)
        }
        window.requestIdleCallback(workLoop)
    })
}

以上就是Vue+NodeJS實(shí)現(xiàn)大文件上傳的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Vue NodeJS大文件上傳的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 基于Vue2實(shí)現(xiàn)動態(tài)折扣表格

    基于Vue2實(shí)現(xiàn)動態(tài)折扣表格

    這篇文章主要為大家詳細(xì)介紹了如何基于Vue2實(shí)現(xiàn)動態(tài)折扣表格,文中的示例代碼講解詳細(xì),具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-01-01
  • vux uploader 圖片上傳組件的安裝使用方法

    vux uploader 圖片上傳組件的安裝使用方法

    這篇文章主要介紹了vux-uploader 圖片上傳組件的安裝及使用方法,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-05-05
  • 用vue實(shí)現(xiàn)注冊頁效果?vue實(shí)現(xiàn)短信驗(yàn)證碼登錄

    用vue實(shí)現(xiàn)注冊頁效果?vue實(shí)現(xiàn)短信驗(yàn)證碼登錄

    這篇文章主要為大家詳細(xì)介紹了用vue實(shí)現(xiàn)注冊頁,短信驗(yàn)證碼登錄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • vue實(shí)現(xiàn)滑動驗(yàn)證條

    vue實(shí)現(xiàn)滑動驗(yàn)證條

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)滑動驗(yàn)證條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • vue實(shí)現(xiàn)氣泡運(yùn)動撞擊效果

    vue實(shí)現(xiàn)氣泡運(yùn)動撞擊效果

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)氣泡運(yùn)動撞擊效,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • vue里面使用mui的彈出日期選擇插件實(shí)例

    vue里面使用mui的彈出日期選擇插件實(shí)例

    今天小編就為大家分享一篇vue里面使用mui的彈出日期選擇插件實(shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-09-09
  • vue中使用 pako.js 解密 gzip加密字符串的方法

    vue中使用 pako.js 解密 gzip加密字符串的方法

    這篇文章主要介紹了vue項(xiàng)目中 使用 pako.js 解密 gzip加密字符串 的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-06-06
  • Vue 實(shí)現(xiàn)html中根據(jù)類型顯示內(nèi)容

    Vue 實(shí)現(xiàn)html中根據(jù)類型顯示內(nèi)容

    今天小編大家分享一篇Vue 實(shí)現(xiàn)html中根據(jù)類型顯示內(nèi)容,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-10-10
  • Vue3+TypeScript封裝axios并進(jìn)行請求調(diào)用的實(shí)現(xiàn)

    Vue3+TypeScript封裝axios并進(jìn)行請求調(diào)用的實(shí)現(xiàn)

    這篇文章主要介紹了Vue3+TypeScript封裝axios并進(jìn)行請求調(diào)用的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • Vue組件通信之父傳子與子傳父深入探究

    Vue組件通信之父傳子與子傳父深入探究

    對于vue來說,組件之間的消息傳遞是非常重要的,用vue可以是要組件復(fù)用的,而組件實(shí)例的作用域是相互獨(dú)立,這意味著不同組件之間的數(shù)據(jù)無法互相引用,一般來說,組件之間可以有幾種關(guān)系,下面是我對組件之間消息傳遞的常用方式的總結(jié)
    2022-12-12

最新評論