原生 JavaScript 实现大文件分片并发上传
首先放上github链接,代码注释的比较清除,上层使用React
进行测试,基本的功能都实现了
技术栈
TypeScript
node
模块
sprak-md5
express
fs
fromidadle
文章目录
实现的功能
- 分片上传
- 验证秒传
- 断点续传
- 并发上传
前言
此项目本来是作为一个react
上传组件编写,编写后发现该组件内部相对比较复杂,于是将该组件抽离出来写成了一个上传工具类
由于分片上传需要后端支持,所以本片会涉及到一点node的知识
为什么需要文件分片上传?
在上传小文件时,分片上传和普通上传的效果体验并不大,但是在上传大文件时,普通的一次性上传会存在以下缺点
- 文件上传时间较长,会长时间持续占用服务器的连接端口
- 如果断网或者页面不小心关闭,已上传的文件会全部丢失,需要重新上传
分片上传的优点
- 充分利用浏览器多进程的特性,并发的上传文件,加快文件的上传速度
- 服务端可以将已经上传的文件切片保存起来,若文件上传过程中出现了意外,再下次上传的时候可以过滤掉已经上传的切片
核心思想
利用H5
提供的原生File对象,由于File
对象是特殊类型的Blob, File
接口也继承了Blob
接口的属性,分片上传的核心思想就是利用File
继承Blob
接口的Blob.slice方法
Blob.slice
方法可以将我们的文件切分为多个单个的切片,分片上传的思想就是利用slice
APi
将文件分割成多个切片,然后利用浏览器多进程的特性进行并发上传
如何实现验证秒传?
思路
在上传切片前将文件的基本信息发送至服务端进行验证,判断该文件是否需要重新上传
验证方法
- 文件名
- 文件最后修改的时间(
File
文件对象的一个属性lastModified
) - 文件hash值
如何选择
每一验证的方法都有其优缺点,文件名
和最后修改的时间
方法相较于计算文件hash
值可以在前端逻辑中快速的判断该文件是否需要上传,但是文件名
和最后修改的时间
无法准确的判断该文件是否需要重传,因为文件名的修改对文件内容无影响,而lastModified
又过于“敏感”,即对文件内容无实质修改的操作也会被记录,从而导致该已上传的文件需要重新上传。
所以我们选用计算文件hash
值的方法判断文件是否需要上传,每次上传文件前首先将文件的hash值发送至服务端判断该文件是否需要重新上传,hash
的计算我们选用spark-md5
,别问为啥,因为spark-md5
已经算是比较快的一个计算hash值的库了
spark-md5
spark-md5
是一个计算文件hash
值的工具
spark-md5
可以帮助我们相对比较快速的计算出文件的hash值,我们可以在服务端存储文件的hash的
spark-md5
基本用法
// 传入的参数为文件切片数组
// 首先递归的将每一个切片可利用FileReader实例读取文件中的内容,在成功的回调函数中将文件添加到spark-md实例中,在递归结束时计算文件hash,并返回文件hash,考虑到该过程比较耗,所以使用promise进行包裹
const calculatehash = (fileChunkList: Array<chunkListsFile>) => (
new Promise(reslove => {
const spark = new SparkMD5.ArrayBuffer()
let count = 0
const loadNext = (index: number) => {
const reader = new FileReader()
reader.readAsArrayBuffer(fileChunkList[index].file)
reader.onload = (e: any) => {
count++
spark.append(e.target.result)
// 如果文件处理完成则发送发送请求
if (count === fileChunkList.length) {
reslove(spark.end())
return
}
loadNext(count)
}
}
loadNext(0)
})
)
实现
前端
因为我们是作为一个组件去写的,所以我们默认不处理html
获取文件对象的步骤,直接从传入文件对象列表开始写,在文件的处理过程中调用传入的参数上报当前的文件处理/上传进度
接口定义
由于开发过程是由ts进行开发,所以我们首先要先定义接口
接口名 | 描述 |
---|---|
fileBasicMessage |
文件基本类型 |
chunkListsFile |
每个文件切片 |
IwaitUploadFile |
待上传文件数组 |
IwaitCalculateFile |
待计算hash文件数组 |
IuploadedFile |
上传完成的文件数组 |
interface fileBasicMessage {
file: File,
id?: string
}
export interface chunkListsFile {
file: Blob
hash: string
fileName: string
index?: number
}
export interface IwaitUploadFile extends fileBasicMessage {
hash?: string
uploadProcess?: number
uploadPercentArr: Array<number> | []
chunkList: Array<chunkListsFile> | []
uploadedSize: number
}
export interface IwaitCalculateFile {
id: string
file: File
}
export interface IuploadedFile {
url: string
fileName: string
}
UML图
工具类大体框架实现
构造一个文件上传类并编写添加文件方法
// 函数需要的参数
export interface Iprops {
// 每个切片的大小
chunkSize?: number
// 每个文件切片列表允许并发上传的个数
concurrency: number
// 上报文件处理上传进度回调函数
updateWaitCalculateFile: (files: Array<IwaitCalculateFile>) => void
updateWaitUploadFile: (files: Array<IwaitUploadFile>) => void
updateUploadedFiles: (files: Array<IuploadedFile>) => void
}
class UploadTool {
// 首先定义我们需要的信息
constructor(props: IProps) {
// 是否正在计算文件hash
this.isCalculating = false
// 切片大小默认4M
this.chunkSize = props.chunkSize ? props.chunkSize : 4 * 1024 * 1024
this.concurrency = props.concurrency ? props.concurrency : 3
this.updateWaitCalculateFile = props.updateWaitCalculateFile
this.updateWaitUploadFile = props.updateWaitUploadFile
this.updateUploadedFiles = props.updateUploadedFiles
this.chunksConcurrenceUploadNum = parseInt(String(10 / this.waitUploadFiles.length))
}
// 添加文件方法 (使用typescript写法)
/**
* @function 对外暴露的添加文件方法
* @param newFiles 新添加的文件数组
*/
public addNewFiles(newFiles: FileList) {
}
/**
* @function 获取文件切片以及hash
*/
private async calculateFilesMessage() {
}
/**
* @function 添加已上传文件并上报
* @param fileName 上传成功的文件名
* @param url 返回的url
*/
private addUploadedFiles(fileName: string, url: string): void {
}
/**
* @function 增加计算hash完成文件并上报 调用上传方法
* @param newWaitUploadFile 计算hash完成的文件
*/
private async addCalculatedFile(newWaitUploadFile: IwaitUploadFile): Promise<any> {
}
/**
* @function 执行验证以及上传逻辑
* @param waitUploadFile 待上传文件信息(内部的参数在接口中已经定义)
*/
private async upload(waitUploadFile: IwaitUploadFile) {
}
/**
* @function 计算已上传的size
* @param AlreadyUploadList 服务端返回的已上传hash列表
* @param waitUploadFile 待上传文件
* @returns 已上传的切片大小
*/
private calculeateAlreadyUploadSize(AlreadyUploadList: Array<any>, waitUploadFile: IwaitUploadFile) {