前端使用koa实现大文件分片上传

目录
  • 引言
  • 前端
    • 拆分上传的文件流
  • 后端
    • 接收文件片段
    • 合并文件片段
  • 总结

引言

一个文件资源服务器,很多时候需要保存的不只是图片,文本之类的体积相对较小的文件,有时候,也会需要保存音视频之类的大文件。在上传这些大文件的时候,我们不可能一次性将这些文件数据全部发送,网络带宽很多时候不允许我们这么做,而且这样也极度浪费网络资源。

因此,对于这些大文件的上传,往往会考虑用到分片传输。

分片传输,顾名思义,也就是将文件拆分成若干个文件片段,然后一个片段一个片段的上传,服务器也一个片段一个片段的接收,最后再合并成为完整的文件。

下面我们来一起简单地实现以下如何进行大文件分片传输。

前端

拆分上传的文件流

首先,我们要知道一点:文件信息的 File 对象继承自 Blob 类,也就是说, File 对象上也存在 slice 方法,用于截取指定区间的 Buffer 数组。

通过这个方法,我们就可以在取得用户需要上传的文件流的时候,将其拆分成多个文件来上传:

<script setup lang='ts'>
import { ref } from "vue"
import { uploadLargeFile } from "@/api"
const fileInput = ref<HTMLInputElement>()
const onSubmit = () => {
  // 获取文件对象
  const file = onlyFile.value?.file;
  if (!file) {
    return
  }
  const fileSize = file.size;  // 文件的完整大小
  const range = 100 * 1024; // 每个区间的大小
  let beginSide = 0; // 开始截取文件的位置
  // 循环分片上传文件
  while (beginSide < fileSize) {
    const formData = new FormData()
    formData.append(
      file.name,
      file.slice(beginSide, beginSide + range),
      (beginSide / range).toString()
    )
    beginSide += range
    uploadLargeFile(formData)
  }
}
</script>
<template>
  <input
    ref="fileInput"
    type="file"
    placeholder="选择你的文件"
  >
  <button @click="onSubmit">提交</button>
</template>

我们先定义一个 onSubmit 方法来处理我们需要上传的文件。

onSubmit 中,我们先取得 ref 中的文件对象,这里我们假设每次有且仅有一个文件,我们也只处理这一个文件。

然后我们定义 一个 beginSiderange 变量,分别表示每次开始截取文件数据的位置,以及每次截取的片段的大小。

这样一来,当我们使用 file.slice(beginSide, beginSide + range) 的时候,我们就取得了这一次需要上传的对应的文件数据,之后便可以使用 FormData 封装这个文件数据,然后调用接口发送到服务器了。

接着,我们使用一个循环不断重复这一过程,直到 beginSide 超过了文件本身的大小,这时就表示这个文件的每个片段都已经上传完成了。当然,别忘了每次切完片后,将 beginSide 移动到下一个位置。

另外,需要注意的是,我们将文件的片添加到表单数据的时候,总共传入了三个参数。第二个参数没有什么好说的,是我们的文件片段,关键在于第一个和第三个参数。这两个参数都会作为 Content-Disposition 中的属性。

第一个参数,对应的字段名叫做 name ,表示的是这个数据本身对应的名称,并不区分是什么数据,因为 FormData 不只可以用作文件流的传输,也可以用作普通 JSON 数据的传输,那么这时候,这个 name 其实就是 JSON 中某个属性的 key

而第二个参数,对应的字段则是 filename ,这个其实才应该真正地叫做文件名。

我们可以使用 wireshark 捕获一下我们发送地请求以验证这一点。

我们再观察上面构建 FormData 的代码,可以发现,我们 appendFormData 实例的每个文件片段,使用的 name 都是固定为这个文件的真实名称,因此,同一个文件的每个片,都会有相同的 name ,这样一来,服务器就能区分哪个片是属于哪个文件的。

filename ,使用 beginSide 除以 range 作为其值,根据上下文语意可以推出,每个片的 filename 将会是这个片的 序号 ,这是为了在后面服务端合并文件片段的时候,作为前后顺序的依据。

当然,上面的代码还有一点问题。

在循环中,我们确实是将文件切成若干个片单独发送,但是,我们知道, http 请求是异步的,它不会阻塞主线程。所以,当我们发送了一个请求之后,并不会等这个请求收到响应再继续发送下一个请求。因此,我们只是做到了将文件拆分成多个片一次性发送而已,这并不是我们想要的。

想要解决这个问题也很简单,只需要将 onSubmit 方法修改为一个异步方法,使用 await 等待每个 http 请求完成即可:

// 省略一些代码
const onSubmit = async () => {
  // ......
  while(beginSide < fileSize) {
    // ......
    await uploadLargeFile(formData)
  }
}
// ......

这样一来,每个片都会等到上一个片发送完成才发送,可以在网络控制台的时间线中看到这一点:

后端

接收文件片段

这里我们使用的 koa-body 来 处理上传的文件数据:

import Router = require("@koa/router")
import KoaBody = require("koa-body")
import { resolve } from 'path'
import { publicPath } from "../common";
import { existsSync, mkdirSync } from "fs"
import { MD5 } from "crypto-js"
const router = new Router()
const savePath = resolve(publicPath, 'assets')
const tempDirPath = resolve(publicPath, "assets", "temp")
router.post(
  "/upload/largeFile",
  KoaBody({
    multipart: true,
    formidable: {
      maxFileSize: 1024 * 1024 * 2,
      onFileBegin(name, file) {
        const hashDir = MD5(name).toString()
        const dirPath = resolve(tempDirPath, hashDir)
        if (!existsSync(dirPath)) {
          mkdirSync(dirPath, { recursive: true })
        }
        if (file.originalFilename) {
          file.filepath = resolve(dirPath, file.originalFilename)
        }
      }
    }
  }),
  async (ctx, next) => {
    ctx.response.body = "done";
    next()
  }
)

我们的策略是先将同一个 name 的文件片段收集到以这个 name 进行 MD5 哈希转换后对应的文件夹名称的文件夹当中,但使用 koa-body 提供的配置项无法做到这么细致的工作,所以,我们需要使用自定义 onFileBegin ,即在文件保存之前,将我们期望的工作完成。

首先,我们拼接出我们期望的路径,并判断这个路径对应的文件夹是否已经存在,如果不存在,那么我们先创建这个文件夹。然后,我们需要修改 koa-body 传给我们的 file 对象。因为对象类型是引用类型,指向的是同一个地址空间,所以我们修改了这个 file 对象的属性, koa-body 最后获得的 file 对象也就被修改了,因此, koa-body 就能够根据我们修改的 file 对象去进行后续保存文件的操作。

这里我们因为要将保存的文件指定为我们期望的路径,所以需要修改 filepath 这个属性。

而在上文中我们提到,前端在 FormData 中传入了第三个参数(文件片段的序号),这个参数,我们可以通过 file.originalFilename 访问。这里,我们就直接使用这个序号字段作为文件片段的名称,也就是说,每个片段最终会保存到 ${tempDir}/${hashDir}/${序号} 这个文件。

由于每个文件片段没有实际意义以及用处,所以我们不需要指定后缀名。

合并文件片段

在我们合并文件之前,我们需要知道文件片段是否已经全部上传完成了,这里我们需要修改一下前端部分的 onSubmit 方法,以发送给后端这个信号:

// 省略一些代码
const onSubmit = async () => {
  // ......
  while(beginSide < fileSize) {
    const formData = new FormData()
    formData.append(
      file.name,
      file.slice(beginSide, beginSide + range),
      (beginSide / range).toString()
    )
    beginSide += range
    // 满足这个条件表示文件片段已经全部发送完成,此时在表单中带入结束信息
    if(beginSide >= fileSize) {
      formData.append("over", file.name)
    }
    await uploadLargeFile(formData)
  }
}
// ......

为图方便,我们直接在一个接口中做传输结束的判断。判断的依据是:当 beiginSide 大于等于 fileSize 的时候,就放入一个 over 字段,并以这个文件的真实名称作为其属性值。

这样,后端代码就可以以是否存在 over 这个字段作为文件片段是否已经全部发送完成的标志:

router.post(
  "/upload/largeFile",
  KoaBody({
    // 省略一些配置
  }),
  async (ctx, next) => {
    if (ctx.request.body.over) { // 如果 over 存在值,那么表示文件片段已经全部上传完成了
      const _fileName = ctx.request.body.over;
      const ext = _fileName.split("\.")[1]
      const hashedDir = MD5(_fileName).toString()
      const dirPath = resolve(tempDirPath, hashedDir)
      const fileList = readdirSync(dirPath);
      let p = Promise.resolve(void 0)
      fileList.forEach(fragmentFileName => {
        p = p.then(() => new Promise((r) => {
            const ws = createWriteStream(resolve(savePath, `${hashedDir}.${ext}`), { flags: "a" })
            const rs = createReadStream(resolve(dirPath, fragmentFileName))
            rs.pipe(ws).on("finish", () => {
              ws.close()
              rs.close();
              r(void 0)
            })
          })
        )
      })
      await p
    }
    ctx.response.body = "done";
    next()
  }
)

我们先取得这个文件真实名字的 hash ,这个也是我们之前用于存放对应文件片段使用的文件夹的名称。

接着我们获取该文件夹下的文件列表,这会是一个字符串数组(并且由于我们前期的设计逻辑,我们不需要在这里考虑文件夹的嵌套)。

然后我们遍历这个数组,去拿到每个文件片段的路径,以此来创建一个读入流,再以存放合并后的文件的路径创建一个写入流(注意,此时需要带上扩展名,并且,需要设置 flags'a' ,表示追加写入),最后以管道流的方式进行传输。

但我们知道,这些使用到的流的操作都是异步回调的。可是,我们保存的文件片段彼此之间是有先后顺序的,也就是说,我们得保证在前面一个片段写入完成之后再写入下一个片段,否则文件的数据就错误了。

要实现这一点,需要使用到 Promise 这一api。

首先我们定义了一个 fulfilled 状态的 Promise 变量 p ,也就是说,这个 p 变量的 then 方法将在下一个微任务事件的调用时间点直接被执行。

接着,我们在遍历文件片段列表的时候,不直接进行读写,而是把读写操作放到 pthen 回调当中,并且将其封装在一个 Promsie 对象当中。在这个 Promise 对象中,我们把 resolve 方法的执行放在管道流的 finish 事件中,这表示,这个 then 回调返回的 Promise 实例,将会在一个文件片段写入完成后被修改状态。此时,我们只需要将这个 then 回调返回的 Promsie 实例赋值给 p 即可。

这样一来,在下个遍历节点,也就是处理第二个文件片段的时候,取得的 p 的值便是上一个文件片段执行完读写操作返回的 Promise 实例,而且第二个片段的执行代码会在第一个片段对应的 Promise 实例 then 方法被触发,也就是上一个片段的文件写入完成之后,再添加到微任务队列。

以此类推,每个片段都会在前一个片段写入完成之后再进行写入,保证了文件数据先后顺序的正确性。

当所有的文件片段读写完成后,我们就拿实现了将完整的文件保存到了服务器。

不过上面的还有许多可以优化的地方,比如:在合并完文件之后,删除所有的文件片段,节省磁盘空间;

使用一个 Map 来保存真实文件名与 MD5 哈希值的映射关系,避免每次都进行 MD5 运算等等。但这里只是给出了简单的实习,具体的优化还请根据实际需求进行调整。

总结

  • 使用 slice 方法可以截取 file 对象的片段,分次发送文件片段;
  • 使用 koa-body 保存每个文件片段到一个指定的暂存文件夹,在文件片段全部发送完成之后,将片段合并。

以上就是前端使用koa实现大文件分片上传的详细内容,更多关于koa大文件分片上传的资料请关注我们其它相关文章!

(0)

相关推荐

  • Node使用koa2实现一个简单JWT鉴权的方法

    JWT 简介 什么是 JWT 全称 JSON Web Token , 是目前最流行的跨域认证解决方案.基本的实现是服务端认证后,生成一个 JSON 对象,发回给用户.用户与服务端通信的时候,都要发回这个 JSON 对象. 该 JSON 类似如下: { "姓名": "张三", "角色": "管理员", "到期时间": "2018年7月1日0点0分" } 为什么需要 JWT 先看下一般的认证

  • koa2服务配置SSL的实现方法

    一:前言 1:SSL证书 我的域名在腾讯云,每次解析新建一个三级域名(假设是  aaa.jiangw1.com ),都会赠送一年的SSL,申请成功后下载SSL证书,如下: 可以看到准备了各种服务器的文件,node服务用红圈中的通用ssl文件即可. 2:解析 aaa.jiangw1.com记录类型填 A ,记录值填服务器公网IP 二:代码 以下代码限定 koa2项目,其余node项目也都类似. 1:安装依赖 npm install koa-sslify npm install koa2-cors

  • 在koa中简单使用Websocket连接的方法示例

    目录 前言 ws模块安装 websocket初始化 websocket下发数据 总结 前言 在一次项目需求会上,有个新需求是要让用户从管理后台主动下发数据到app前端,从而让前端那边对这主动下发的数据做一些用户交互.实现思路很清晰,用Websocket的方式.Websocket 是一种自然的全双工.双向.单套接字连接,是建立在 TCP 协议上的. 相比于 HTTP 协议,Websocket 链接一旦建立,即可进行双向的实时通信: ws模块安装 由于后台是基于node+koa2+mongo进行开发

  • node koa2 ssr项目搭建的方法步骤

    一.创键项目 1.创建目录 koa2 2.npm init 创建 package.json,然后执行 npm install 3.通过 npm install koa 安装 koa 模块 4.通过 npm install supervisor 安装supervisor模块, 用于node热启动 5.在根目录下中新建 index.js 文件,作为入口文件, 内容如下: const Koa = require('koa'); // Koa 为一个class const app = new Koa()

  • 使用 Koa + TS + ESLlint 搭建node服务器的过程详解

    目录 初始化项目 环境准备 安装环境 初始化 tsconfig.json 简单搭建 Koa 服务器 完整项目搭建 依赖安装 构建目录结构 修改 package.json 代码规范 eslint prettier 初始化项目 环境准备 与之前使用JavaScript 开发后台不同,区别如下: 自动编译运行的插件由nodemon替换为ts-node-dev. 在TypeScript环境下,需要使用到ES6模块化规范.而非CommonJS规则. 使用TypeScript语法进行开发,再开发结束后,需要

  • 使用nodejs + koa + typescript 集成和自动重启的问题

    目录 版本说明 创建项目 安装依赖 填充内容 src/server.ts tsconfig.json package.json 运行 参考资料 版本说明 Node.js: 16.13.1 创建项目 创建如下目录结构 project ├── src │ └── server.ts ├── package.json └── tsconfig.json package.json 可以使用 yarn init -y 生成 tsconfig.json 可以使用 tsc --init 生成(需要全局或在项目

  • 前端使用koa实现大文件分片上传

    目录 引言 前端 拆分上传的文件流 后端 接收文件片段 合并文件片段 总结 引言 一个文件资源服务器,很多时候需要保存的不只是图片,文本之类的体积相对较小的文件,有时候,也会需要保存音视频之类的大文件.在上传这些大文件的时候,我们不可能一次性将这些文件数据全部发送,网络带宽很多时候不允许我们这么做,而且这样也极度浪费网络资源. 因此,对于这些大文件的上传,往往会考虑用到分片传输. 分片传输,顾名思义,也就是将文件拆分成若干个文件片段,然后一个片段一个片段的上传,服务器也一个片段一个片段的接收,最

  • Java超详细大文件分片上传代码

    目录 Java 大文件分片上传 首先是交互的控制器 上传文件分片参数接收 大文件分片上传服务类实现 文件分片上传定义公共服务类接口 文件分片上传文件操作接口实现类 OSS阿里云对象存储分片上传实现 京东云对象存储实现 腾讯云对象存储分片上传 分片上传前端代码实现 Java 大文件分片上传 原理:前端通过js读取文件,并将大文件按照指定大小拆分成多个分片,并且计算每个分片的MD5值.前端将每个分片分别上传到后端,后端在接收到文件之后验证当前分片的MD5值是否与上传的MD5一致,待所有分片上传完成之

  • PHP大文件分片上传的实现方法

    一.前言 在网站开发中,经常会有上传文件的需求,有的文件size太大直接上传,经常会导致上传过程中耗时太久,大量占用带宽资源,因此有了分片上传. 分片上传主要是前端将一个较大的文件分成等分的几片,标识当前分片是第几片和总共几片,待所有的分片均上传成功的时候,在后台进行合成文件即可. 二.开发过程中遇到的问题 分片的时候每片该分多大size?太大会出现"413 request entity too large" 分片上传的时候并不是严格按照分片的序号顺序上传,如何判断所有的分片均上传成功

  • .NET Core Web APi大文件分片上传研究实现

    前言 前两天发表利用FormData进行文件上传,然后有人问要是大文件几个G上传怎么搞,常见的不就是分片再搞下断点续传,动动手差不多也能搞出来,只不过要深入的话,考虑的东西还是很多.由于断点续传之前写个几篇,这里试试利用FormData来进行分片上传. .NET Core Web APi文件分片上传 这里我们依然是使用FormData来上传,只不过在上传之前对文件进行分片处理,如下HTML代码 <div class="form-horizontal" style="ma

  • Java实现浏览器端大文件分片上传

    目录 背景介绍 项目介绍 需要知识点 启动项目 项目示范 核心讲解 核心原理 功能分析 分块上传 秒传功能 断点续传 总结 参考文献 背景介绍   Breakpoint-http,是不是觉得这个名字有点low,break point断点.这是一个大文件上传的一种实现.因为本来很久没写过前端了,本来想自己好好写一番js,可惜因为种种原因而作罢了.该项目是基于一款百度开源的前端上传控件:WebUploader(百度开源的东西文档一如既往的差,哈哈.或者是我理解能力差).   Breakpoint-h

  • React+Node实现大文件分片上传、断点续传秒传思路

    目录 1.整体思路 2.实现步骤 2.1 文件切片加密 2.2 查询上传文件状态 2.3 秒传 2.4 上传分片.断点续传 2.5 合成分片还原完整文件 3.总结 4.后续扩展与思考 5.源码 1.整体思路 将文件切成多个小的文件: 将切片并行上传: 所有切片上传完成后,服务器端进行切片合成: 当分片上传失败,可以在重新上传时进行判断,只上传上次失败的部分实现断点续传: 当切片合成为完整的文件,通知客户端上传成功: 已经传到服务器的完整文件,则不需要重新上传到服务器,实现秒传功能: 2.实现步骤

  • webuploader在springMVC+jquery+Java开发环境下的大文件分片上传的实例代码

    注意: 1,webuploader上传组件会和jQuery自带的上传组件冲突,所以不要使用<form>标签中添加上传文件的属性; enctype="multipart/form-data" 2.并且屏蔽ApplicationContext-mvc.xml里面的拦截配置! <!-- 上传拦截,如最大上传值及最小上传值 --> <!--新增加的webuploader上传组件,必须要屏蔽这里的拦截机制 <bean id="multipartRes

  • JavaScript实现大文件分片上传处理

    很多时候我们在处理文件上传时,如视频文件,小则几十M,大则 1G+,以一般的HTTP请求发送数据的方式的话,会遇到的问题: 1.文件过大,超出服务端的请求大小限制: 2.请求时间过长,请求超时: 3.传输中断,必须重新上传导致前功尽弃 这些问题很影响用户的体验感,所以下面介绍一种基于原生JavaScript进行文件分片处理上传的方案,具体实现过程如下: 1.通过dom获取文件对象,并且对文件进行MD5加密(文件内容+文件标题形式),采用SparkMD5进行文件加密: 2.进行分片设置,文件Fil

  • vue 大文件分片上传(断点续传、并发上传、秒传)

    对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送.接收都是不可取,很容易导致内存问题.所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率. 本文是基于 springboot + vue 实现的文件上传,本文主要介绍vue实现文件上传的步骤及代码实现,服务端(springboot)的实现步骤及实现请移步本人的另一篇文章: springboot 大文件上传.分片上传.断点续传.秒传 上传分步: 本人分析上传总共分为: MD5读取文件,获取文件的

  • 利用Vue3+Element-plus实现大文件分片上传组件

    目录 一.背景 二.技术栈 三.核心代码实现 四.总结 一.背景 实际项目中遇到需要上传几十个G的3d模型文件,传统上传就不适用了. 结合element提供的上传组件自己封装了文件分片上传的组件. 思路: 把文件拆分成若干分片 依次上传分片(每次上传前可校验该分片是否已经上传) 发起合并分片的请求 二.技术栈 Vue3+Ts+Element-Plus 其他库:spark-md5 后端接口: 上传分片接口 校验分片是否已上传接口 合并分片接口 三.核心代码实现 Element组件基础配置 <el-

随机推荐