基于websocket的大文件上传

2,211 阅读2分钟

实现思路

  1. 运用websocket建立一个长链接
  2. 发送整个文件的md5,分片总量,当前分片序号,当前片的二进制流
  3. 根据后端返回数据补充等,进行下一步操作

1.websocket实现

进入页面时建立websocket链接,当链接发生问题时,重新链接

import { ref, computed } from 'vue'
import { message } from 'ant-design-vue'
export default function useSocket() {
  const webSocket = ref(null)
  const total = ref(0)
  function newSocket() {
    if (!webSocket.value) {
      webSocket.value = new WebSocket('ws://10.0.86.154:8080')
      webSocket.value.onopen = function() {
        console.log('连接服务器成功!')
        total.value = 0
      }
      webSocket.value.onclose = function() {
        console.log('服务器关闭')
        webSocket.value = null
        if (total.value <= 10) {
          setTimeout(() => {
            newSocket()
            total.value++
          }, 3000)
        } else {
          message.error('服务器断开连接', 3)
        }
      }
      webSocket.value.onerror = function() {
        console.log('连接出错')
        webSocket.value.close()
      }
    }
  }
  return {
    newSocket,
    webSocket,
  }
}

2.文件切片以及上传

二进制无法携带其他信息,根据后端返回需要片的需要进行上传

  function getFile(file, i) {
    const name = file.name
    const size = file.size
    const shardSize = 1024 * 1024// 以1MB为一个分片
    const shardCount = Math.ceil(size / shardSize) // 总片数
    if (i > shardCount || i < 0) return
    const start = (i - 1) * shardSize
    const end = Math.min(size, start + shardSize)
    const fileBlob = file.slice(start, end)
    const reader = new FileReader()
    // 二进制流
    reader.onload = ev => {
      const buffer = ev.target.result
      if (!stop.value) return
      webSocket.value.send(JSON.stringify({ userId: userInfo.value.uid, fileName:file.name,currentSlice: i, totalSlice: shardCount, fileMd5: zipMd5.value }))
      webSocket.value.send(buffer)
    }
    reader.readAsArrayBuffer(fileBlob)
  }

上传文件使用base64编码,用bufferedAmount检测未发送的字节数

function getFile(file, i) {
    const name = file.name
    const size = file.size
    const shardSize = 1024 * 256 // 以0.5MB为一个分片
    const shardCount = Math.ceil(size / shardSize) // 总片数
    const start = (i - 1) * shardSize
    const end = Math.min(size, start + shardSize)
    const fileBlob = file.slice(start, end)
    if (i > shardCount) return
    if (webSocket.value.bufferedAmount < shardSize * 5) {
      const reader = new FileReader()
      // Base64
      reader.onload = (ev) => {
        webSocket.value.send(JSON.stringify({userId: userInfo.value.uid,fileName: file.name,currentSlice: i,totalSlice: shardCount,fileMd5: zipMd5.value,buffer: ev.target.result}))
        getFile(file, i + 1)
      }
      reader.readAsDataURL(fileBlob)
    } else {
      setTimeout(() => {
        getFile(file, i)
      }, 500)
    }
  }

md5加密

  function jsMd5(file) {
    const reader = new FileReader()
    reader.readAsArrayBuffer(file)
    return new Promise((resolve, reject) => {
      reader.onload = ev => {
        const buffer = ev.target.result
        const sign = md5(buffer)
        resolve(sign)
      }
    })
  }

总体代码

import useSocket from './socket.js'
const { newSocket, webSocket } = useSocket()
import { onMounted, ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import md5 from 'js-md5'

import { useStore } from 'vuex'
export default function useFile() {
 const store = useStore()
 const userInfo = computed(() => store.getters['user/userInfo'])

 // 文件相关
 const fileList = ref([])
 const disabled = ref(false)
 const zipMd5 = ref()
 const fileName = ref()

 // 进度条
 const progress = ref({})
 const percent = ref([0, 0, 0])
 const stop = ref(false)

 // 文件上传
 const beforeUpload = (file) => {
   if (file.type !== 'application/x-zip-compressed') {
     message.error('文件拓展名不为zip格式', 3)
     return false
   }
   if (fileList.value.length === 0) fileList.value.push(file)
   else fileList.value[0] = file
   stop.value = true
   fileName.value = file.name
   disabled.value = true
   customUpload(fileList.value[0])
   return false
 }
 // 自定义操作指令
 function customUpload(file) {
   if (!webSocket.value) {
     newSocket()
   }
   jsMd5(file).then((res) => {
     zipMd5.value = res
     getFile(file, 1)
     webSocket.value.onmessage = function(e) {
       const res = JSON.parse(e.data)
       if (res.errorCode === '00500') {
         message.error(res.errorMessage, 3)
         del()
       } else {
         progress.value = res.data
         percent.value[res.data.step - 1] = res.data.fileProgress
         if (res.data.step === 1) getFile(file, res.data.currentSlice)
         if (res.data.step === 3 && res.data.fileProgress === 100) {
           disabled.value = false
           message.success('导入成功', 3)
           store.commit('app/setimport', true)
           del()
         }
       }
     }
   })
 }
 function getFile(file, i) {
   const name = file.name
   const size = file.size
   const shardSize = 1024 * 1024// 以0.5MB为一个分片
   const shardCount = Math.ceil(size / shardSize) // 总片数
   //
   if (i > shardCount || i < 0) return
   const start = (i - 1) * shardSize
   const end = Math.min(size, start + shardSize)
   const fileBlob = file.slice(start, end)
   const reader = new FileReader()
   // 二进制流
   reader.onload = ev => {
     const buffer = ev.target.result
     if (!stop.value) return
     webSocket.value.send(JSON.stringify({ userId: userInfo.value.uid, fileName: file.name, currentSlice: i, totalSlice: shardCount, fileMd5: zipMd5.value }))
     webSocket.value.send(buffer)
   }
   reader.readAsArrayBuffer(fileBlob)
 }
 function del() {
   webSocket.value.send(JSON.stringify({ msgType: 'stop' }))
   percent.value = [0, 0, 0]
   fileName.value = ''
   fileList.value = []
   disabled.value = false
   store.commit('app/setimport', true)
   stop.value = false
 }
 function jsMd5(file) {
   const reader = new FileReader()
   reader.readAsArrayBuffer(file)
   return new Promise((resolve, reject) => {
     reader.onload = ev => {
       const buffer = ev.target.result
       const sign = md5(buffer)
       resolve(sign)
     }
   })
 }
 onMounted(() => {
   newSocket()
 })
 return {
   beforeUpload,
   del,
   fileList,
   disabled,
   progress,
   percent,
   fileName
 }
}

<template>
 <div>
   <a-upload-dragger
     :fileList="[]"
     name="file"
     :maxCount="1"
     :beforeUpload="beforeUpload"
     :disabled="disabled"
   >
     <p class="ant-upload-drag-icon">
       <CloudUploadOutlined />
     </p>
     <p class="ant-upload-text">点击或将文件拖拽到这里上传</p>
     <p class="ant-upload-hint">文件扩展名:zip</p>
   </a-upload-dragger>
 </div>
 <div v-show="fileList.length != 0" class="fileProgress">
   <div class="fileProgrName">
       <div>
         <PaperClipOutlined />&nbsp;
         {{fileName}}
       <span>{{progress.msg ||''}}...</span>
       </div>

       <div @click="del">
         <DeleteOutlined />
       </div>
   </div>
   <div class="progress">
     <a-progress
       v-model:percent="percent[0]"
       size="small"
       status="active"
       :show-info="false"
     />
     <a-progress
       v-model:percent="percent[1]"
       size="small"
       status="active"
       :show-info="false"
     />
     <a-progress
       v-model:percent="percent[2]"
       size="small"
       status="active"
       :show-info="false"
     />
   </div>
 </div>

</template>
<script setup>
import { ref } from 'vue'
import { CloudUploadOutlined, PaperClipOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import useFile from './hooks/file.js'
const { beforeUpload, del, fileName, disabled, fileList, progress, percent } = useFile()
</script>
<style lang='less' scoped>
.fileProgress{
.progress{
 display: flex;
}
.fileProgrName{
 display: flex;
 justify-content: space-between;
 span{
   color: rgba(0, 0, 0, 0.4);
 }
}
}
</style>