前端文件分片上传流程
介绍
作为一个前端,平常工作中肯定离不开文件上传的业务需求,可能大部分情况都是对图片,附件等一些小文件进行上传,但是当需要上传大文件的时候,使用普通上传方式时,可能就会看到接口返回413
,这里就需要提到本文所要说的文件分片上传。
正文
二进制数据类型
前端对二进制数据的存储格式有很多种。
input
输入框上传文件时用到的File类型。
- 视频网站上能看到的
blob
链接资源
es6
中出现的ArrayBuffer
,并且是文件分片上传,断点续传的基础。
- 还有一个就是
base64
不同类型的相互转换
很多数据类型的转换都需要依靠于FileReader
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)
|
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次
- 对文件进行分片
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
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)
formData.append("md5", md5)
axios.post("/path/to/upload", formData)
|
后端通过返回下一分片的索引或者是下一分片的范围来指定前端接下来需要上传的分片
在上一步骤中也可以返回和这一步骤相同的内容。
- 文件上传完成通知
这一步骤为可选步骤,通过前端通知后端完成了所有文件分片的上传。
一般不需要前端主动向后端通知。
后端实现
这里简单使用node
讲解一下后端接收文件的基本逻辑
使用koa
做基础服务
使用koa-body
接收前端的文件内容
使用koa-router
做对应的restful设计
- 文件存在性检查
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 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) })
|
- 文件合并
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
上传组件