聚是⼀团⽕散作满天星,前端Vue.js+elementUI结合后端FastAPI实现⼤⽂件分
⽚上传
分⽚上传并不是什么新概念,尤其是⼤⽂件传输的处理中经常会被使⽤,在之前的⼀篇⽂章⾥:我们讨论了如何读写超⼤型⽂件,本次再来探讨⼀下如何上传超⼤型⽂件,其实原理都是⼤同⼩异,原则就是化整为零,将⼤⽂件进⾏分⽚处理,切割成若⼲⼩⽂件,随后为每个分⽚创建⼀个新的临时⽂件来保存其内容,待全部分⽚上传完毕后,后端再按顺序读取所有临时⽂件的内容,将数据写⼊新⽂件中,最后将临时⽂件再删掉。⼤体流程请见下图:
其实现在市⾯上有很多前端的三⽅库都集成了分⽚上传的功能,⽐如百度的WebUploader,遗憾的是它已经淡出历史舞台,⽆⼈维护了。现在⽐较推荐主流的库是vue-simple-uploader,不过饿了么公司开源的elementUI市场占有率还是⾮常⾼的,但其实⼤家所不知道的是,这个⾮常著名的前端UI库也已经许久没⼈维护了,Vue3.0版本出来这么久了,也没有做适配,由此可见⼤公司的开源产品还是需要给业务让步。本次我们利⽤elementUI的⾃定义上传结合后端的⽹红框架FastAPI来实现分⽚上传。
⾸先前端需要安装需要的库:
npm install element-ui --save
npm install spark-md5 --save
npm install axios --save
随后在⼊⼝⽂件main.js中进⾏配置:
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
import Axios from 'axios'
Vue.prototype.axios = Axios;
import QS from 'qs'
Vue.prototype.qs = QS;
配置好之后,设计⽅案,前端通过elementUI上传时,通过分⽚⼤⼩的阈值对⽂件进⾏切割,并且记录每⼀⽚⽂件的切割顺序(chunk),在这个过程中,通过SparkMD5来计算⽂件的唯⼀标识(防⽌多个⽂件同时上传的覆盖问题identifier),在每⼀次分⽚⽂件的上传中,会将分⽚⽂件实体,切割顺序(chunk)以及唯⼀标识(identifier)异步发送到后端接⼝(fastapi),后端将chunk和identifier结合在⼀起作为临时⽂件写⼊服务器磁盘中,当前端将所有的分⽚⽂件都发送完毕后,最后请求⼀次后端另外⼀个接⼝,后端将所有⽂件合并。
根据⽅案,前端建⽴chunkupload.js⽂件:
import SparkMD5 from 'spark-md5'
//错误信息
function getError(action, option, xhr) {
let msg
if (sponse) {
msg = `${ || sponse}`
msg = `${ || sponse}`
} else if (sponseText) {
msg = `${sponseText}`
} else {
msg = `fail to post ${action} ${xhr.status}`
}
const err = new Error(msg)
err.status = xhr.status
err.url = action
return err
}
// 上传成功完成合并之后,获取服务器返回的json
function getBody(xhr) {
const text = sponseText || sponse
if (!text) {
return text
}
try {
return JSON.parse(text)
} catch (e) {
return text
}
}
// 分⽚上传的⾃定义请求,以下请求会覆盖element的默认上传⾏为
export default function upload(option) {
if (typeof XMLHttpRequest === 'undefined') {
return
}
const spark = new SparkMD5.ArrayBuffer()// md5的ArrayBuffer加密类
const fileReader = new FileReader()// ⽂件读取类
const action = option.action // ⽂件上传上传路径
const chunkSize = 1024 * 1024 * 1 // 单个分⽚⼤⼩,这⾥测试⽤1m
let md5 = ''// ⽂件的唯⼀标识
const optionFile = option.file // 需要分⽚的⽂件
let fileChunkedList = [] // ⽂件分⽚完成之后的数组
const percentage = [] // ⽂件上传进度的数组,单项就是⼀个分⽚的进度
// ⽂件开始分⽚,push到fileChunkedList数组中,并⽤第⼀个分⽚去计算⽂件的md5      for (let i = 0; i < optionFile.size; i = i + chunkSize) {
const tmp = optionFile.slice(i, Math.min((i + chunkSize), optionFile.size))
if (i === 0) {
}
fileChunkedList.push(tmp)
}
// 在⽂件读取完毕之后,开始计算⽂件md5,作为⽂件唯⼀标识
spark.append(sult)
md5 = d() + new Date().getTime()
console.log('⽂件唯⼀标识--------', md5)
// 将fileChunkedList转成FormData对象,并加⼊上传时需要的数据
fileChunkedList = fileChunkedList.map((item, index) => {
const formData = new FormData()
if (option.data) {
// 额外加⼊外⾯传⼊的data数据
Object.keys(option.data).forEach(key => {
formData.append(key, option.data[key])
})
// 这些字段看后端需要哪些,就传哪些,也可以⾃⼰追加额外参数
formData.append(option.filename, item, option.file.name)// ⽂件
formData.append('chunkNumber', index + 1)// 当前⽂件块
formData.append('chunkSize', chunkSize)// 单个分块⼤⼩
formData.append('currentChunkSize', item.size)// 当前分块⼤⼩
formData.append('totalSize', optionFile.size)// ⽂件总⼤⼩
formData.append('identifier', md5)// ⽂件标识
formData.append('filename', option.file.name)// ⽂件名
formData.append('totalChunks', fileChunkedList.length)// 总块数            }
return { formData: formData, index: index }
})
// 更新上传进度条百分⽐的⽅法
const updataPercentage = (e) => {
let loaded = 0// 当前已经上传⽂件的总⼤⼩
percentage.forEach(item => {
loaded += item
})
e.percent = loaded / optionFile.size * 100
}
// 创建队列上传任务,limit是上传并发数,默认会⽤两个并发
function sendRequest(chunks, limit = 2) {
return new Promise((resolve, reject) => {
const len = chunks.length
let counter = 0
let isStop = false
const start = async () => {
if (isStop) {
return
}
const item = chunks.shift()
console.log()
if (item) {
const xhr = new XMLHttpRequest()
const index = item.index
// 分⽚上传失败回调
isStop = true
reject(e)
}
// 分⽚上传成功回调
if (xhr.status < 200 || xhr.status >= 300) {
isStop = true
reject(getError(action, option, xhr))
}
if (counter === len - 1) {
// 最后⼀个上传完成
resolve()
} else {
counter++
start()
}
}
// 分⽚上传中回调
if (xhr.upload) {
progress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100
}
percentage[index] = e.loaded
console.log(index)
updataPercentage(e)
}
}
xhr.open('post', action, true)
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true
}
const headers = option.headers || {}
for (const item in headers) {
if (headers.hasOwnProperty(item) && headers[item] !== null) {                                  xhr.setRequestHeader(item, headers[item])
}
}
// ⽂件开始上传
xhr.send(item.formData);
}
}
while (limit > 0) {
setTimeout(() => {
start()
}, Math.random() * 1000)
limit -= 1
}
})
}
try {
// 调⽤上传队列⽅法等待所有⽂件上传完成
await sendRequest(fileChunkedList,2)
// 这⾥的参数根据⾃⼰实际情况写
const data = {
identifier: md5,
filename: option.file.name,
totalSize: optionFile.size
}
// 给后端发送⽂件合并请求
const fileInfo = await this.axios({
前端页面模板method: 'post',
url: 'localhost:8000/mergefile/',
data: this.qs.stringify(data)
}, {
headers: {
"Content-Type": "multipart/form-data"
}
}).catch(error => {
console.log("ERRRR:: ", sponse.data);
});
console.log(fileInfo);
if (de === 200) {
const success = quest)
return
}
} catch (error) {
}
}
}
之后建⽴upload.vue模板⽂件,并且引⼊⾃定义上传控件:
<template>
<div>
<el-upload
:http-request="chunkUpload"
:ref="chunkUpload"
:action="uploadUrl"
:data="uploadData"
:
on-error="onError"
:before-remove="beforeRemove"
name="file" >
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
</div>
</template>
<script>
//js部分
import chunkUpload from './chunkUpload'
export default {
data() {
return {
uploadData: {
//这⾥⾯放额外携带的参数
},
//⽂件上传的路径
uploadUrl: 'localhost:8000/uploadfile/', //⽂件上传的路径
chunkUpload: chunkUpload // 分⽚上传⾃定义⽅法,在头部引⼊了    }
},
methods: {
onError(err, file, fileList) {
this.$s.chunkUploadXhr.forEach(item => {
item.abort()
})
this.$alert('⽂件上传失败,请重试', '错误', {
confirmButtonText: '确定'
})
},
beforeRemove(file) {
// 如果正在分⽚上传,则取消分⽚上传
if (file.percentage !== 100) {
this.$s.chunkUploadXhr.forEach(item => {
item.abort()
})
}
}
}
}
</script>
<style>
</style>

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。