文件分片上传

前端文件分片上传流程

介绍

作为一个前端,平常工作中肯定离不开文件上传的业务需求,可能大部分情况都是对图片,附件等一些小文件进行上传,但是当需要上传大文件的时候,使用普通上传方式时,可能就会看到接口返回413,这里就需要提到本文所要说的文件分片上传。

正文

二进制数据类型

前端对二进制数据的存储格式有很多种。

  • input输入框上传文件时用到的File类型。
  • 视频网站上能看到的blob链接资源
  • es6中出现的ArrayBuffer,并且是文件分片上传,断点续传的基础。
  • 还有一个就是base64

不同类型的相互转换

很多数据类型的转换都需要依靠于FileReader

  1. ArrayBuffer to Blob
1
2
3
4
5
6
7
8
9
const blob = new Blob([new ArrayBuffer(1024)])  
const fileReader = new FileReader()

fileReader.onload = function() {
console.log(fileReader.result)
}

fileReader.readAsArrayBuffer(blob)

  1. Blob to Base64
1
2
3
4
5
6
7
8
const blob = new Blob([new ArrayBuffer(1024)])  
const fileReader = new FileReader()

fileReader.onload = function() {
console.log(fileReader.result)
}

fileReader.readAsDataURL(blob)

以上是两个简单的数据类型转换的例子

需要使用到的npm package

  • spark-md5
    spark-md5可以将对应的数据进行md5加密,这样在下一次对同一个文件进行上传时,可以直接跳过整个上传流传,实现秒传的效果。

具体流程

  • input接收文件template.mp4
  • 获取文件的基础信息
    • 文件的mime,当前的类型是video/mp4
    • 文件的大小file.size,假设本次文件的大小为1G
    • 设置本次上传分片的大小为5M
    • 计算本次上传的分片数量为1G / 5M = 205,所以本次需要上传205次
  • 对文件进行分片
    • 使用spark-md5进行分片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ArrayBuffer } from 'spark-md5'

const SparkMd5 = new ArrayBuffer()

// 保存分片用于在后面上传时用
const cacheChunks = []
// 当前分片的索引
const index = 0
// 分片的大小
const chunkSize = 1024 * 1024 * 5
// file 只需要简单调用 file.slice 就可以
const currentChunk = file.slice(index * chunkSize, (index + 1) * chunkSize)

cacheChunks.push(currentChunk)

// 添加分片
SparkMd5.append(currentChunk)

// 所有分片添加完成时
const md5 = SparkMd5.end()

// 完成解析后销毁对应实例
SparkMd5.destroy()

  • 文件存在性检查
    将文件的一些信息传递给后台,服务端检查文件是否存在。
    如果存在,则跳过之后的上传过程。
    如果不存在,则继续后面的上传流程。

  • 文件分片上传
    按照索引顺序对文件分片进行上传

1
2
3
4
5
6
7
8
9
10
11
12
import axios from 'axios'

const formData = new FormData()
// 需要上传的文件分片
formData.append("file", chunk)
// 当前分片的索引
formData.append("index", index)
// 文件md5
formData.append("md5", md5)

axios.post("/path/to/upload", formData)

后端通过返回下一分片的索引或者是下一分片的范围来指定前端接下来需要上传的分片
在上一步骤中也可以返回和这一步骤相同的内容。

  • 文件上传完成通知
    这一步骤为可选步骤,通过前端通知后端完成了所有文件分片的上传。
    一般不需要前端主动向后端通知。

后端实现

这里简单使用node讲解一下后端接收文件的基本逻辑
使用koa做基础服务
使用koa-body接收前端的文件内容
使用koa-router做对应的restful设计

  1. 文件存在性检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

let fileCache = {}

router
.get("/", async (ctx) => {
const { md5, chunkSize, filename, size, length } = ctx.query
if(fileCache[md5] && fileCache[md5].chunks.length === length) {
ctx.body = {
success: true,
res: {
data: true
}
}
return
}else if(fileCache[md5]) {
const index = findLastUnUploadChunkIndex()
ctx.body = {
success: true,
res: {
data: index
}
}
}else {
fileCache[md5] = {
chunks: [],
size,
length,
chunkSize,
md5
}
ctx.body = {
success: true,
res: {
data: 0
}
}
}
})
  1. 文件上传
1
2
3
4
5
6
7
router.post("/", async (ctx) => {
const files = ctx.request.files.file
const { md5, index } = ctx.request.body
fileCache[md5].chunks.push(index)
// 使用fs保存对应的分片至指定文件夹
// 分片名称可以使用md5-index的形式
})
  1. 文件合并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const fs = require("fs")
const fsPromise = fs.promises

// 合并分片
async function mergeChunk() {

const realFilePath = "/path/to/real/file"

// 获取对应的文件分片文件名称
const chunkList = await fsPromise.readdir("/path/to/chunk")

// 对文件进行排序
chunkList.sort((suffixA, suffixB) => Number(suffixA.split('-')[1]) - Number(suffixB.split('-')[1]))

//文件合并
const mergeTasks = async () => {
for(let i = 0; i < chunkList.length; i ++) {
const chunk = chunkList[i]
await fs.readFile(chunk)
.then(data => fs.appendFile(realFilePath, data))
.then(_ => fs.unlink(chunk))
}
}

return fs.writeFile(realFilePath, '')
.then(mergeTasks)
.catch(err => {})
}

结束

以上就是整个文件分片上传的流程,当中的具体细节可以查看本人完成的对应的上传工具类库,还有基于该类库封装的React上传组件