原文转载自「刘悦的技术博客」https://v3u.cn/a_id_175

分片上传并不是什么新概念,尤其是大文件传输的处理中经常会被使用,在之前的一篇文章里: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

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_175

axios 上传文件_聚是一团火散作满天星,前端Vue.js+elementUI结合后端FastAPI实现大文件分片上传...相关推荐

  1. vue.js — 安装Webpake创建一个完整的项目并上传至码云

    vue.js - 安装Webpake创建一个完整的项目并上传至码云 今天总结一下之前几天学习的一整套的创建项目方法: 前提条件:已安装node.js.npm/cnpm最新版本.vue-cli. VS ...

  2. 推荐 7 个 Vue.js 插件,也许你的项目用的上(四)

    加速你的 Vue.js 开发 当我们可以通过使用库轻松实现相同的结果时,为什么还要编写自定义功能?开发人员最好的朋友和救星就是这些第三方库.我相信一个好的项目会利用一些可用的最佳库.Vue.js 是创 ...

  3. 推荐 7 个 Vue.js 插件,也许你的项目用的上(三)

    使用这7个库 Vue.js 库,加快你的项目开发! 当我们可以通过使用库轻松实现相同的结果时,为什么还要编写自定义功能?开发人员最好的朋友和救星就是这些第三方库.我相信一个好的项目会利用一些可用的最佳 ...

  4. 前后端分离跨服务器文件上传,SpringBoot+Vue.js实现前后端分离的文件上传功能

    这篇文章需要一定vue和springboot的知识,分为两个项目,一个是前端vue项目,一个是后端springboot项目. 后端项目搭建 我使用的是springboot1.5.10+jdk8+ide ...

  5. html 前端优化上传视频,前端上传组件Plupload使用---上传大视频(分片上传)

    上传视频到服务器 1.引入js插件: 2.html页面如图: 上传视频: 上传视频 支持AVI.wma.rmvb.rm.flash.mp4.mid.3GP等格式 3.js代码 $(function ( ...

  6. web页超过2G以上大视频分片秒传方案

    本文的视频上传方案是基于乐视视频点播做的原理分析 var videoUploadEntity = (function () {function videoUploadEntity() {}videoU ...

  7. 前后端配合实现大文件断点续传(前端逻辑)

    断点续传作用:当上传文件时,大文件上传时耗时过长,如果遇到网络卡顿.断网等情况,再重新开始上传的体验感非常不好.前端优化分片上传文件,上传时把大文件分成很多个小文件,等到网络状况恢复了之后,即从之前上 ...

  8. vue理由设置_在你的下一个Web应用中使用Vue.js的三个理由

    Vue.js是那么地易上手,它在提供了大量开箱即用的功能的同时也提供了良好的性能.请继续阅读以下事例及代码片段以便更加了解Vue.js. 选择一个JavaScript框架真是太难了--因为有太多的框架 ...

  9. 推荐 7 个 Vue.js 插件,也许你的项目用的上(一)

    当我们可以通过使用库轻松实现相同的结果时,为什么还要编写自定义功能?开发人员最好的朋友和救星就是这些第三方库.我相信一个好的项目会利用一些可用的最佳库.Vue.js 是创建用户界面的最佳 JavaSc ...

最新文章

  1. sqoop mysql parquet_sqoop一些语法的使用
  2. 不知道这些AI术语,还敢说你很了解AI吗?
  3. strace oracle
  4. linux间服务器间文件传输,Linux命令scp服务器间文件传输教程
  5. python中变量的类型是动态的随时可以变化_python动态类型简介
  6. qt编译实现简单的文本编译器有粘贴复制_qmake 时复制文件(自动在编译前做一些操作,且写在.pro文件里)...
  7. 关于安装Ubuntu后触摸板无法使用的解决方案
  8. 聊聊Elasticsearch的Iterables
  9. jQuery动画效果之上卷下拉
  10. zuul网关_SpringCould之服务网关(zuul)介绍与配置
  11. CentOS下配置多个Tomcat同时运行 本篇文章来源于 Linux公社网站(www.linuxidc.com)
  12. 【WebService笔记02】使用CXF框架实现WebService接口的发布和调用
  13. APPweb测试工具
  14. 算丰征途「SOPHON盘古无人驾驶系统」基本框架介绍
  15. Q245R正火控扎一探-20℃冲击容器板切割,舞钢Q245R-20℃冲击
  16. 天眼查 乱码 java_反爬虫解析-字体替换(天眼查/猫眼电影)
  17. 3种结构ZnO基半导体纳米复合材料-图文详解
  18. 【2023校招刷题】常见面试问题总结(一、EDA工具及IC整体设计流程篇)(随后续面试不断更新....)
  19. 解决 Android Bitmap 合成图片时 PNG透明背景 为黑色的问题
  20. 阿里云——专有网络VPC

热门文章

  1. 【SimMechanics】使用Matlab/SimMechanics仿真机械臂
  2. 机器学习--线性回归4(线性拟合、局部线性拟合实战)
  3. Vue学习笔记之06-响应式的数组方法
  4. 鸿蒙系统非手机用,【图片】华为鸿蒙系统的厉害之处在于 你可能非用不可 !【手机吧】_百度贴吧...
  5. Java线程池线程突然没了_70%人答不全!线程池中的一个线程异常了会被怎么处理?...
  6. ant design pro模板_ant design pro 当中改变ant design 组件的样式和 数据管理
  7. 无法嵌入互操作类型NationalInstruments.TestStand.Interop.UI.ExecutionViewOptions。请改用适用的接口...
  8. 修改官方发行openstack镜像的cloud-init登录方式为账号密码登录
  9. HDU6064 Besttheorem
  10. 【转】Pro Android学习笔记(二五):用户界面和控制(13):LinearLayout和TableLayout...