聚是一团火散作满天星,前端Vue.js+elementUI结合后端FastAPI实现大文件分片上传
分片上传并不是什么新概念,尤其是大文件传输的处理中经常会被使用,在之前的一篇文章里:python花式读取大文件(10g/50g/1t)遇到的性能问题(面试向)我们讨论了如何读写超大型文件,本次再来探讨一下如何上传超大型文件,其实原理都是大同小异,原则就是化整为零,将大文件进行分片处理,切割成若干小文件,随后为每个分片创建一个新的临时文件来保存其内容,待全部分片上传完毕后,后端再按顺序读取所有临时文件的内容,将数据写入新文件中,最后将临时文件再删掉。大体流程请见下图:
其实现在市面上有很多前端的三方库都集成了分片上传的功能,比如百度的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 (xhr.response) { msg = `${xhr.response.error || xhr.response}` } else if (xhr.responseText) { msg = `${xhr.responseText}` } else { msg = `fail to post ${action} ${xhr.status}` } const err = new Error(msg) err.status = xhr.status err.method = 'post' err.url = action return err
}
// 上传成功完成合并之后,获取服务器返回的json
function getBody(xhr) { const text = xhr.responseText || xhr.response 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) { fileReader.readAsArrayBuffer(tmp) } fileChunkedList.push(tmp) } // 在文件读取完毕之后,开始计算文件md5,作为文件唯一标识 fileReader.onload = async (e) => { spark.append(e.target.result) md5 = spark.end() + 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 option.onProgress(e) } // 创建队列上传任务,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 // 分片上传失败回调 xhr.onerror = function error(e) { isStop = true reject(e) } // 分片上传成功回调 xhr.onload = function onload() { 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) { xhr.upload.onprogress = 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: 'http://localhost:8000/mergefile/', data: this.qs.stringify(data) }, { headers: { "Content-Type": "multipart/form-data" } }).catch(error => { console.log("ERRRR:: ", error.response.data); }); console.log(fileInfo); if (fileInfo.data.code === 200) { const success = getBody(fileInfo.request) option.onSuccess(success) return } } catch (error) { option.onError(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: 'http://localhost:8000/uploadfile/', //文件上传的路径 chunkUpload: chunkUpload // 分片上传自定义方法,在头部引入了 } }, methods: { onError(err, file, fileList) { this.$store.getters.chunkUploadXhr.forEach(item => { item.abort() }) this.$alert('文件上传失败,请重试', '错误', { confirmButtonText: '确定' }) }, beforeRemove(file) { // 如果正在分片上传,则取消分片上传 if (file.percentage !== 100) { this.$store.getters.chunkUploadXhr.forEach(item => { item.abort() }) } } }
} </script> <style> </style>
这里定义的后端上传接口是:http://localhsot:8000/uploadfile/ 合并文件接口是:http://localhsot:8000/mergefile/
此时启动前端的vue.js服务:
npm run dev
页面效果见下图:
前端搞定了,下面我们来编写接口,后端的任务相对简单,利用FastAPI接收分片文件、分片顺序以及唯一标识,并且将文件临时写入到服务器中,当最后一个分片文件完成上传后,第二个接口负责按照分片顺序合并所有文件,合并成功后再删除临时文件,用来节约空间,先安装依赖的三方库
pip3 install python-multipart
当然了,由于是前后端分离项目,别忘了设置一下跨域,编写main.py:
from uploadfile import router
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from model import database
from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [ "*"
]
app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
) app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") app.include_router(router) @app.on_event("startup")
async def startup(): await database.connect() @app.on_event("shutdown")
async def shutdown(): await database.disconnect() @app.get("/")
def read_root(): return {"Hello": "World"}
然后编写uploadfile.py:
@router.post("/uploadfile/")
async def uploadfile(file: UploadFile = File(...), chunkNumber: str = Form(...), identifier: str = Form(...)): task = identifier # 获取文件唯一标识符 chunk = chunkNumber # 获取该分片在所有分片中的序号 filename = '%s%s' % (task,chunk) # 构成该分片唯一标识符 contents = await file.read() #异步读取文件 with open('./static/upload/%s' % filename, "wb") as f: f.write(contents) print(file.filename) return {"filename": file.filename} @router.post("/mergefile/")
async def uploadfile(identifier: str = Form(...), filename: str = Form(...)): target_filename = filename # 获取上传文件的文件名 task = identifier # 获取文件的唯一标识符 chunk = 1 # 分片序号 with open('./static/upload/%s' % target_filename, 'wb') as target_file: # 创建新文件 while True: try: filename = './static/upload/%s%d' % (task,chunk) # 按序打开每个分片 source_file = open(filename, 'rb') # 读取分片内容写入新文件 target_file.write(source_file.read()) source_file.close() except IOError: break chunk += 1 os.remove(filename) return {"code":200}
值得一提的是这里我们使用UploadFile来定义文件参数,它的优势在于在接收存储文件过程中如果文件过大超过了内存限制就会存储在硬盘中,相当灵活,同时配合await关键字异步读取文件内容,提高了性能和效率。
启动后端服务测试一下效果:
uvicorn main:app --reload
可以看到,当我们上传一张2.9m的图片时,前端会根据设置好的的分片阈值将该图片切割为四份,传递给后端接口uploadfile后,后端在根据参数用接口mergefile将其合并,整个过程一气呵成、行云流水、势如破竹,让人用了之后禁不住心旷神怡、把酒临风。最后奉上项目地址:https://gitee.com/QiHanXiBei/fastapi_blog
聚是一团火散作满天星,前端Vue.js+elementUI结合后端FastAPI实现大文件分片上传相关推荐
- axios 上传文件_聚是一团火散作满天星,前端Vue.js+elementUI结合后端FastAPI实现大文件分片上传...
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_175 分片上传并不是什么新概念,尤其是大文件传输的处理中经常会被使用,在之前的一篇文章里:python花式读取大文件(10g/50 ...
- php用什么上传代码,php常用文件怎么上传_PHP常用文件上传代码_一聚教程网
在PHP的使用中,它有着非常方便的操作设计,这次文章就给大家介绍下怎么使用PHP实现常用文件上传,相信这也是大多数人会遇到的问题,下面我们具体看看上传方法. _path = $path; $this- ...
- 梵高:每个人心中都有一团火,而路过的人只看到了烟
文森特 • 威廉 • 梵 • 高(Vincent Willem van Gogh,1853-1890),荷兰后印象派画家.出生于牧师家庭,是后印象主义的先驱,并深深地影响了二十世纪艺术,尤其是野兽派与 ...
- 写在前面--点燃酱爆心中的那团火
感谢在浩瀚互联网世界里,游历至此的您!有缘千里来相会,文字的相会,博客的会见.缘分?猿粪?曾几何时一直想经营一个自己的博客.记录自己的心绪,分享自己的技术,反思自己的行为,局促自己的成长.这个想法在脑 ...
- 每个人的心里都有一团火,路过的人只看到烟。
每个人的心里都有一团火,路过的人只看到烟. 但是总有一个人,总有那么一个人能看到这团火,然后走过来,陪我一起. 我在人群中,看到了他的火,然后快步走过去, 生怕慢一点他就会被淹没在岁月的尘埃里. 我带 ...
- 上传失败 已保存至草稿_学霸的草稿纸火了,卷面整洁,网友调侃:作业都比不上这草稿本...
文章原创,版权归本作者所有,欢迎个人转发分享! 学霸和学渣的区别,从很多方面能考究出来,听课时的专心程度,写作业时的细心程度,以及草稿纸的工整程度.相比随意应付,井井有条,才是学霸的正确打开方式. 学 ...
- 作用于HTML元素的Vue.js指令
我在这里学习内置指令 v-model v-if v-show v-else v-for v-bind v-on v-model表单数据模型双向绑定 example: ① 表单文本输入框效果 <i ...
- 微信小程序商城源码独立版/公众号/H5/DIY装修/营销/直播/拼团/秒杀/前端vue全开源代码
软件优势: 一键开关商城功能,模板消息,diy自定义商城首页.会员中心. hinkphp6国内流行的PHP框架,结构代码清晰,适合中小企业快速开发应用. uniapp一套代码多个平台,开发不浪费,账号 ...
- 2021-10-08 vue.js实现抖音很火八卦时间数字罗盘屏保壁纸
vue.js实现抖音很火八卦时间数字罗盘屏保壁纸 代码如下. <!DOCTYPE html> <html><head> <meta charset=" ...
- android好玩的独立游戏排行榜,安卓独立游戏十大神作:大叔向前冲上榜,第一名画面十分唯美...
百无聊赖时我们总需要一些东西来打发时间同时带我们体验闲下来的乐趣,这时一款好玩又不烧脑的游戏就显得十分重要.那么,就让小编来为大家推荐几款安卓独立游戏神作以供大家在工作之余放松一下. 1.鲤 属于创意 ...
最新文章
- E-learning的现状与未来
- Java实现单链表的反转
- Nginx动静分离-tomcat
- 解决chrome和firefox flash不透明的方法
- Office365开发系列——开发一个全功能的Word Add-In
- 每天20分钟,只需一年,一年级学生英语听力达到六年级水平!关键是坚持一点都不难!
- JAVA JFrame画图基础和事件监听
- 【Java】jdk和eclipse下载安装
- 用html设计一个logo页面_如何设计一个Logo?——Bobu Africa旅行品牌Logo设计
- 优秀!Python神器NumPy 论文终登上了 顶刊Nature!
- php 检查txt中全角大写字母的个数
- 实验记录一 初步接触cortex-M3
- 报错:非介入式客户端验证规则中的验证类型名称必须唯一。下列验证类型出现重复...
- 如何识别媒体偏见_面部识别技术存在偏见:为什么我们不应该盲目相信新技术
- 简述计算机数控系统的工作原理,计算机数控系统与802D系统编程基本原理
- 计算机屏幕重影红色,Win7/Win10系统玩游戏屏幕出现红蓝重影解决方法
- 深度网络梯度爆炸的原因、产生的影响和解决方法(常用激活函数)
- Python编程之找完数
- Compose版来啦!仿自如裸眼3D效果
- 软工实践 - 第十一次作业 Alpha 冲刺 (3/10)