实现思路
- 运用websocket建立一个长链接
- 发送整个文件的md5,分片总量,当前分片序号,当前片的二进制流
- 根据后端返回数据补充等,进行下一步操作
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 />
{{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>