大规格文件的上传优化思路详解

在开发过程中,收到这样一个问题反馈,在网站上传 100 MB 以上的文件经常失败,重试也要等老半天,这就难为需要上传大规格文件的用户了。那么应该怎么做才能快速上传,就算失败了再次发送也能从上次中断的地方继续上传呢?下文为你揭晓答案~

温馨提示:配合 Demo 源码一起阅读效果更佳

整体思路

第一步是结合项目背景,调研比较优化的解决方案。
文件上传失败是老生常谈的问题,常用方案是将一个大文件切片成多个小文件,并行请求接口进行上传,所有请求得到响应后,在服务器端合并所有的分片文件。当分片上传失败,可以在重新上传时进行判断,只上传上次失败的部分,减少用户的等待时间,缓解服务器压力。这就是分片上传文件。

大文件上传

那么如何实现大文件分片上传呢?

流程图如下:

分为以下步骤实现:

1. 文件 MD5 加密

MD5 是文件的唯一标识,可以利用文件的 MD5 查询文件的上传状态。

根据文件的修改时间、文件名称、最后修改时间等信息,通过 spark-md5 生成文件的 MD5。需要注意的是,大规格文件需要分片读取文件,将读取的文件内容添加到 spark-md5 的 hash 计算中,直到文件读取完毕,最后返回最终的 hash 码到 callback 回调函数里面。这里可以根据需要添加文件读取的进度条。

实现方法如下:

// 修改时间+文件名称+最后修改时间-->MD5
md5File (file) {
 return new Promise((resolve, reject) => {
 let blobSlice =
  File.prototype.slice ||
  File.prototype.mozSlice ||
  File.prototype.webkitSlice
 let chunkSize = file.size / 100
 let chunks = 100
 let currentChunk = 0
 let spark = new SparkMD5.ArrayBuffer()
 let fileReader = new FileReader()
 fileReader.onload = function (e) {
  console.log('read chunk nr', currentChunk + 1, 'of', chunks)
  spark.append(e.target.result) // Append array buffer
  currentChunk++
  if (currentChunk < chunks) {
  loadNext()
  } else {
  let cur = +new Date()
  console.log('finished loading')
  // alert(spark.end() + '---' + (cur - pre)); // Compute hash
  let result = spark.end()
  resolve(result)
  }
 }
 fileReader.onerror = function (err) {
  console.warn('oops, something went wrong.')
  reject(err)
 }
 function loadNext () {
  let start = currentChunk * chunkSize
  let end =
  start + chunkSize >= file.size ? file.size : start + chunkSize
  fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
 }
 loadNext()
 })
}

2. 查询文件状态

前端得到文件的 MD5 后,从后台查询是否存在名称为 MD5 的文件夹,如果存在,列出文件夹下所有文件,得到已上传的切片列表,如果不存在,则已上传的切片列表为空。

// 校验文件的MD5
checkFileMD5 (file, fileName, fileMd5Value, onError) {
 const fileSize = file.size
 const { chunkSize, uploadProgress } = this
 this.chunks = Math.ceil(fileSize / chunkSize)
 return new Promise(async (resolve, reject) => {
 const params = {
  fileName: fileName,
  fileMd5Value: fileMd5Value,
 }
 const { ok, data } = await services.checkFile(params)
 if (ok) {
  this.hasUploaded = data.chunkList.length
  uploadProgress(file)
  resolve(data)
 } else {
  reject(ok)
  onError()
 }
 })
}

3. 文件分片

文件上传优化的核心就是文件分片,Blob 对象中的 slice 方法可以对文件进行切割,File 对象是继承 Blob 对象的,因此 File 对象也有 slice 方法。

定义每一个分片文件的大小变量为 chunkSize,通过文件大小 FileSize 和分片大小 chunkSize 得到分片数量 chunks,使用 for 循环和 file.slice() 方法对文件进行分片,序号为 0 - n,和已上传的切片列表做比对,得到所有未上传的分片,push 到请求列表 requestList。

async checkAndUploadChunk (file, fileMd5Value, chunkList) {
 let { chunks, upload } = this
 const requestList = []
 for (let i = 0; i < chunks; i++) {
 let exit = chunkList.indexOf(i + '') > -1
 // 如果已经存在, 则不用再上传当前块
 if (!exit) {
  requestList.push(upload(i, fileMd5Value, file))
 }
 }
 console.log({ requestList })
 const result =
 requestList.length > 0
  ? await Promise.all(requestList)
  .then(result => {
   console.log({ result })
   return result.every(i => i.ok)
  })
  .catch(err => {
   return err
  })
  : true
 console.log({ result })
 return result === true
}

4. 上传分片

调用 Promise.all 并发上传所有的切片,将切片序号、切片文件、文件 MD5 传给后台。

后台接收到上传请求后,首先查看名称为文件 MD5 的文件夹是否存在,不存在则创建文件夹,然后通过 fs-extra 的 rename 方法,将切片从临时路径移动切片文件夹中,结果如下:

当全部分片上传成功,通知服务端进行合并,当有一个分片上传失败时,提示“上传失败”。在重新上传时,通过文件 MD5 得到文件的上传状态,当服务器已经有该 MD5 对应的切片时,代表该切片已经上传过,无需再次上传,当服务器找不到该 MD5 对应的切片时,代表该切片需要上传,用户只需上传这部分切片,就可以完整上传整个文件,这就是文件的断点续传。

// 上传chunk
upload (i, fileMd5Value, file) {
 const { uploadProgress, chunks } = this
 return new Promise((resolve, reject) => {
 let { chunkSize } = this
 // 构造一个表单,FormData是HTML5新增的
 let end =
  (i + 1) * chunkSize >= file.size ? file.size : (i + 1) * chunkSize
 let form = new FormData()
 form.append('data', file.slice(i * chunkSize, end)) // file对象的slice方法用于切出文件的一部分
 form.append('total', chunks) // 总片数
 form.append('index', i) // 当前是第几片
 form.append('fileMd5Value', fileMd5Value)
 services
  .uploadLarge(form)
  .then(data => {
  if (data.ok) {
   this.hasUploaded++
   uploadProgress(file)
  }
  console.log({ data })
  resolve(data)
  })
  .catch(err => {
  reject(err)
  })
 })
}

5. 上传进度

虽然分片批量上传比大文件单次上传会快很多,也还是有一段加载时间,这时应该加上上传进度的提示,实时显示文件上传进度。

原生 Javascript 的 XMLHttpRequest 有提供 progress 事件,这个事件会返回文件已上传的大小和总大小。项目使用 axios 对 ajax 进行封装,可以在 config 中增加 onUploadProgress 方法,监听文件上传进度。

const config = {
 onUploadProgress: progressEvent => {
 var complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
 }
}
services.uploadChunk(form, config)

6. 合并分片

上传完所有文件分片后,前端主动通知服务端进行合并,服务端接受到这个请求时主动合并切片,通过文件 MD5 在服务器的文件上传路径中找到同名文件夹。从上文可知,文件分片是按照分片序号命名的,而分片上传接口是异步的,无法保证服务器接收到的切片是按照请求顺序拼接。所以应该在合并文件夹里的分片文件前,根据文件名进行排序,然后再通过 concat-files 合并分片文件,得到用户上传的文件。至此大文件上传就完成了。

Node 端代码:

// 合并文件
exports.merge = {
 validate: {
 query: {
  fileName: Joi.string()
  .trim()
  .required()
  .description('文件名称'),
  md5: Joi.string()
  .trim()
  .required()
  .description('文件md5'),
  size: Joi.string()
  .trim()
  .required()
  .description('文件大小'),
 },
 },
 permission: {
 roles: ['user'],
 },
 async handler (ctx) {
 const { fileName, md5, size } = ctx.request.query
 let { name, base: filename, ext } = path.parse(fileName)
 const newFileName = randomFilename(name, ext)
 await mergeFiles(path.join(uploadDir, md5), uploadDir, newFileName, size)
  .then(async () => {
  const file = {
   key: newFileName,
   name: filename,
   mime_type: mime.getType(`${uploadDir}/${newFileName}`),
   ext,
   path: `${uploadDir}/${newFileName}`,
   provider: 'oss',
   size,
   owner: ctx.state.user.id,
  }
  const key = encodeURIComponent(file.key)
   .replace(/%/g, '')
   .slice(-100)
  file.url = await uploadLocalFileToOss(file.path, key)
  file.url = getFileUrl(file)
  const f = await File.create(omit(file, 'path'))
  const files = []
  files.push(f)
  ctx.body = invokeMap(files, 'toJSON')
  })
  .catch(() => {
  throw Boom.badData('大文件分片合并失败,请稍候重试~')
  })
 },
}

总结

本文讲述了大规格文件上传优化的一些做法,总结为以下 4 点:

  • ob.slice 将文件切片,并发上传多个切片,所有切片上传后告知服务器合并,实现大文件分片上传;
  • 原生 XMLHttpRequest 的 onprogress 对切片上传进度的监听,实时获取文件上传进度;
  • spark-md5 根据文件内容算出文件 MD5,得到文件唯一标识,与文件上传状态绑定;
  • 分片上传前通过文件 MD5 查询已上传切片列表,上传时只上传未上传过的切片,实现断点续传。

参照 Demo 源码 可快速上手上述功能

到此这篇关于大规格文件的上传优化思路详解的文章就介绍到这了,更多相关大文件上传优化内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript使用享元模式实现文件上传优化操作示例

    本文实例讲述了JavaScript使用享元模式实现文件上传优化操作.分享给大家供大家参考,具体如下: 一.享元模式是一种用于性能优化的模式,主要优化方式为,若系统中因为创建了大量类似的对象而导致内存占用过高,则可以考虑使用享元模式实现. 二.实例说明: 如果在浏览器中上传文件,若使用常规写法, 每上传一个文件,就会创建一个实例对象:如果上传2000个文件,那就有2000个对象,浏览器很可能出现假死状态.这种情况下,我们考虑适应享元模式. 三.实例: var Upload = function(

  • CI框架实现优化文件上传及多文件上传的方法

    本文实例分析了CI框架实现优化文件上传及多文件上传的方法.分享给大家供大家参考,具体如下: 最近一直在研究Codeigniter框架,开发项目写到文件上传的时候发现大部分程序员使用Codeigniter框架的文件上传类编写上传方法的时候写的都存在这代码冗余(或者说代码重复利用率低.比较消耗资源.)故而我研究出一个稍微优化一点的上传方法.并且在查找资料时发现,Codeigniter框架同时上传多个文件比较困难,所以在优化方法的同时我又研究了一下如何使用Codeigniter框架实现同时上传多个文件

  • 大规格文件的上传优化思路详解

    在开发过程中,收到这样一个问题反馈,在网站上传 100 MB 以上的文件经常失败,重试也要等老半天,这就难为需要上传大规格文件的用户了.那么应该怎么做才能快速上传,就算失败了再次发送也能从上次中断的地方继续上传呢?下文为你揭晓答案~ 温馨提示:配合 Demo 源码一起阅读效果更佳 整体思路 第一步是结合项目背景,调研比较优化的解决方案. 文件上传失败是老生常谈的问题,常用方案是将一个大文件切片成多个小文件,并行请求接口进行上传,所有请求得到响应后,在服务器端合并所有的分片文件.当分片上传失败,可

  • Servlet文件的上传与下载详解

    目录 文件的上传和下载 1. 文件上传细节 2. 文件上传 3. 文件下载 文件的上传和下载 1. 文件上传细节 要有一个 form 标签,method-post请求 (因为get有长度限制) form标签的属性 encType 值必须为 multipart/form-data 表示提交的数据以多端(每一个表单项一个数据段)的形式进行拼接,然后以二进制流的形式发送给服务器 在 form 标签中使用 input type="file" 添加上传的文件 编写服务器代码 (Servlet接收

  • Java 文件上传的实例详解

    Java 文件上传的实例详解 java 文件上传 Java文件上传,介绍几种常用的方法,也是经过本人亲手调试过的 1.jspsmartupload 这个组件用起来是挺方便的,不过就是只适合小文件上传,如果大文件上传的话就不行,查看了一下他的代码,m_totalBytes = m_request.getContentLength(); m_binArray = new byte[m_totalBytes];居然把整个上传文件都读到内存去了,那如果是上传几十M的文件,同时几个用户上传,服务器稳挂,不

  • 最详细的文件上传下载实例详解(推荐)

    在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现. 对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用Servlet获取上传文件的输入流然后再解析里面的请求参数是比较麻烦,所以一般选择采用apache的开源工具common-fileupload这个文件上传组件.这个common-fileupload上传组件的jar包可以去apache官网上面下载,也可以在struts的lib文件夹下面找到,stru

  • Bootstrap Fileinput文件上传组件用法详解

    最近时间空余,总结了一些关于bootstrap fileinput组件的一些常见用法,特此分享到我们平台,供大家参考,同时也方便以后的查找.本文写的不好还请见谅. 一.效果展示 1.原始的input type='file',简直不忍直视. 2.不做任何装饰的bootstrap fileinput:(bootstrap fileinput初级进化) 3.bootstrap fileinput高级进化:中文化.可拖拽上传.文件扩展名校验(如果不是需要的文件,不让上传) 拖拽上传 上传中 4.boot

  • jQuery File Upload文件上传插件使用详解

    jQuery File Upload 是一个Jquery文件上传组件,支持多文件上传.取消.删除,上传前缩略图预览.列表显示图片大小,支持上传进度条显示:支持各种动态语言开发的服务器端. 官网链接:https://github.com/blueimp/jQuery-File-Upload/wiki 特点:拖放支持:上传进度条:图像预览:可定制和可扩展的:兼容任何服务器端应用平台(PHP, Python, Ruby on Rails, Java, Node.js, Go etc.). 使用方法:

  • SpringBoot整合MinIO实现文件上传的方法详解

    目录 前言 1. MinIO 简介 2. MinIO 安装 3. 整合 Spring Boot 4. 配置nginx 5. 小结 前言 现在 OSS 服务算是一个基础服务了,很多云服务厂商都有提供这样的服务,价格也不贵,松哥自己的网站用的就是类似的服务. 不过对于中小公司来说,除了购买 OSS 服务之外,也可以自己搭建专业的文件服务器,自己搭建专门的文件服务器的话,曾经比较专业的做法是 FastDFS,松哥之前也专门为之录过视频发在 B 站上,感兴趣的小伙伴可以自行查看.不过 FastDFS 搭

  • Spring实现文件上传的配置详解

    添加依赖 主要用来解析request请求流,获取文件字段名.上传文件名.content-type.headers等内容组装成FileItem <!--添加fileupload依赖--> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.3</versio

  • SpringBoot实现项目文件上传的方法详解

    目录 一.首先抛出问题 二.解决思路 三.直接看源码就懂了,下面是controller 四.结尾 一.首先抛出问题 以阿里云oss文件上传为例,假设我的需求是这样的,我需要发布一条动态,这条动态呢可以是图片.语音.视频,3种类型,每种类型的上传我必须要限制它的文件大小,超过了我就不能让他上传的.如果传统的方式,那就是创建3个上传类型bucket对应图片.语音和视频,其实这种做法是可以的,但是怎么说呢,还不够优雅,如果当这个动态有越来越多种类型,你是不是要建立N个类型对应呢,所以就会使得bucke

  • React文件分段上传实现方法详解

    目录 原理 方案 antd Upload 文件分片 MD5 发送分片请求 显示上传进度 最近做了大文件(文件夹)分片上传的需求,记录一下. 原理 前端进行大文件分片上传的方案几乎都是利用Blob.prototype.slice方法对文件进行分片,用数组将每一个分片存起来,最后将分片发给后端.由于并发的原因,需要给每个分片给定index,方便后端进行拼接. 方案 我在做需求之前看了网上的一些方案,大多数是前端进行分片.发送分片,在发送完所有分片请求之后,再给后端发送一个合并文件的请求.但其实也可以

随机推荐