上传方案

117 阅读14分钟

实现思路

  1. 创建一个上传文件的类Uploader
  2. 创建上传文件的主类 Uploader,该类主要暴露方法添加文件,暂停和继续
  3. 用来计算md5的类CalcMd5
  4. 用于处理上传文件块的类

定义上传文件的状态和需要暴露的方法

  1. 首先定义上传的状态,该状态是每个上传的文件必须经过的过程
enum FileStatus{
	init = "init", // 等待获取md5
    reading = 'reading', // 正在获取md5
    waiting = 'waiting', // 等待上传中
    uploading = 'uploading', // 上传中
    paused = 'paused', // 暂停
    marge = 'marge', // 合并中
    uploaded = 'uploaded', // 上传完成
    error = 'error', // 上传失败
    exist = 'exist'  // 已存在
}
  1. 定义一个上传的类,该类有三个公开的方法,分别为添加文件、暂停、继续
class Uploader{

	public addFile(file: File){
	
	}

	public paused(uid: string){
	
	}
	
	public resume(uid: string){

	}
}

添加文件到上传列表

  1. 对这个添加的文件进行封装
import { v4 as uuidv4 } from 'uuid';

class UploaderFile{
	public uid: string;              // 文件的唯一id,因为生成MD5是个异步操作,先用它做唯一id
	public raw: File;                // 需要上传的文件实例
	public md5: string = "";         // 文件的MD5,初始化先为空
	public status = FileStatus.init; // 文件的上传状态
	public readProgress = 0;         // 文件读取MD5的进度,主要针对大文件
	public progress=0;               // 上传进度
	public speed = 0                 // 上传速度
	public _lastProgressCallback: number = 0;   // 上一次请求返回的时间
	public _prevUploadedSize: number = 0;       // 上一次请求块的大小

	constructor(file:File){
		this.raw = file
		this.uid = uuidv4()
	}
}
  1. 创建上传的文件的实例对象
class Uploader{

	public addFile(file: File){
+		const uploadItem = new UploaderFile(file)		
	}

	...
}
  1. 需要一个数组存放添加的文件实例
class Uploader{

+	public uploadList: UploaderFile[] = [] // 上传的文件列表

	public addFile(file: File){
		const uploadItem = new UploaderFile(file);
+		this.uploadList.push(uploadItem)
	}
}

计算文件的MD5

添加文件到列表这一步完成了,接下来就是我们计算该文件的MD5

  1. 先定义一个计算md5的类, 公开属性是否正在计算MD5 公开的方法addFile,该方法接收一个参数,需要计算MD5的待上传文件的实例 私有属性默认分片的大小
class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算md5

	public async addFile(uploadItem: UploaderFile){
		
	}
}
  1. 我们用一个私有属性存放正在计算MD5的上传文件的实例
class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem
	}

}
  1. 再定义一个获取文件MD5的私有方法,该方法为异步方法
class CalcMd5 {

	...

	private getFileMd5(){
		return new Promise((resolve, reject) =>{
			
		}
	}
}
  1. 我们再添加上传文件实例的公开方法中使用该方法
class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算Md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem
+       this.isCalcMd5 = true
+       let md5 = await this.getFileMd5();
+       this.isCalcMd5 = false
	}

	private getFileMd5(){
		return new Promise((resolve, reject) =>{
			
		}
	}
}

  1. 要计算文件的MD5,就需要先对文件进行分块, 我们创建一个获取文件分片的函数,主要传两个参数,文件对象和分片的大小
function getChunks(file: File, chunkSize: number){
	if(file.size===0){
		return [file]
	}
	
	// 该文件的块数
	const count = Math.ceil(file.size / chunkSize);
	let chunks: Blob[] = [];   // 需要存储的文件分片

	for (let i = 0; i < count; i++){
		let chunk: Blob;
		if (i === count - 1){
			// 该文件的最后一个分片
			chunk = file.slice(i * chunkSize, file.size)
		}else{
			chunk = file.slice(i * chunkSize, i * chunkSize + chunkSize)
		}
		 chunks.push(chunk)
	}

	return chunks
}
  1. 计算MD5,我们需要获取文件的分片,并遍历该分片
class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算Md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem
	    this.isCalcMd5 = true
		let md5 = await this.getFileMd5();
	    this.isCalcMd5 = false
	}

	private getFileMd5(){
		return new Promise((resolve, reject) =>{

			// 获取文件的分片
+			const chunks = getChunks(this.uploadItem?.raw,1024 * 1024 * 10 );

+			for(let i=0;i<chunks.length;i++){
					
+			}
		}
	}
}
  1. 我们可以将读取文件,并计算MD5的方法封装成一个方法
+import SparkMD5 from 'spark-md5';

class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算Md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem
	    this.isCalcMd5 = true
		let md5 = await this.getFileMd5();
	    this.isCalcMd5 = false
	}

	private getFileMd5(){
		return new Promise((resolve, reject) =>{
		 
			const spark = new SparkMD5.ArrayBuffer();

+			const appendToSpark = (chunk: Blob) => {
+			    return new Promise(resolve => {
+		            const reader = new FileReader();
+                    reader.readAsArrayBuffer(chunk);
+                    reader.onload = (e: any) => {
+		                spark.append(e.target.result)
+                        resolve(spark)
+		            }
+		        })
+			}

			// 获取文件的分片
			const chunks = getChunks(this.uploadItem?.raw, 1024 * 1024 * 10);

			for(let i=0;i<chunks.length;i++){
					
			}
		}
	}
}
  1. 由于读取文件的方法为异步方法。使用该方法时要在使用该方法的上下文函数中加async
import SparkMD5 from 'spark-md5';

class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem
	    this.isCalcMd5 = true
		let md5 = await this.getFileMd5();
	    this.isCalcMd5 = false
	}

	private getFileMd5(){
+		return new Promise(async (resolve, reject) =>{
		 
			const spark = new SparkMD5.ArrayBuffer();

			const appendToSpark = (chunk: Blob) => {
			    return new Promise(resolve => {
			        const reader = new FileReader();
	                reader.readAsArrayBuffer(chunk);
                    reader.onload = (e: any) => {
		                spark.append(e.target.result)
                        resolve(spark)
		            }
		        })
			}

			// 获取文件的分片
			const chunks = getChunks(this.uploadItem?.raw,1024 * 1024 * 10);

			for(let i=0;i<chunks.length;i++){
+				await appendToSpark(chunks[i])	
			}
		}
	}
}

  1. 对文件的分片计算完MD5后就可以返回结果了
import SparkMD5 from 'spark-md5';

class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem
	    this.isCalcMd5 = true
		let md5 = await this.getFileMd5();
	    this.isCalcMd5 = false
	}

	private getFileMd5(){
		return new Promise(async (resolve, reject) =>{
		 
			const spark = new SparkMD5.ArrayBuffer();

			const appendToSpark = (chunk: Blob) => {
			    return new Promise(resolve => {
			        const reader = new FileReader();
	                reader.readAsArrayBuffer(chunk);
                    reader.onload = (e: any) => {
		                spark.append(e.target.result)
                        resolve(spark)
		            }
		        })
			}

			// 获取文件的分片
			const chunks = getChunks(this.uploadItem?.raw, 1024 * 1024 * 10);

			for(let i=0;i<chunks.length;i++){
				await appendToSpark(chunks[i])	
			}

+			let md5 = spark.end()
+			resolve(md5)
		}
	}
}

在上传的类中使用计算MD5的方法

  1. 在上传的类中添加私有属性,计算MD5类的实例
class Uploader {

+	private calcMd5: CalcMd5 = new CalcMd5();

	...
}
  1. 在上传的类中添加一个计算MD5的私有方法
class Uploader{

	public uploadList: UploaderFile[] = [] // 上传的文件列表
	private calcMd5: CalcMd5 = new CalcMd5();

	...

+	private calcMd5(){
		// 判断是否有文件正在计算MD5,因为计算MD5时密集性操作,所以多个文件不能同时计算MD5
+		if (!this.calcMd5.isCalcMd5){
			// 从上传的实例列表中过滤出还未计算MD5的文件实例
+			const uploadList = this.uploadList.filter(item => item.status === FileStatus.init)

+			if(uploadList.length > 0){
+				this.calcMd5.addFile(uploadList[0])
+			}
		}
+	}
}
  1. 将文件添加到上传的列表之后,就可以计算MD5了
class Uploader{

	public uploadList: UploaderFile[] = [] // 上传的文件列表
	private calcMd5: CalcMd5 = new CalcMd5();

	public addFile(file: File){
		const uploadItem = new UploaderFile(file);
		this.uploadList.push(uploadItem)
+		this.calcMd5();
	}

	private calcMd5(){
		...
	}
}
  1. 在计算md5的类中,我们需要更新上传文件的状态,由于对象的内存相同,我们可以直接修改状态
class CalcMd5{

	public isCalcMd5: boolean = false;             // 是否正在计算md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem

		// 此时该上传文件的实例状态变为正在读取MD5
		this.uploadItem.status = FileStatus.reading
	
	    this.isCalcmd5 = true
		let md5 = await this.getFileMd5();
	    this.isCalcMd5 = false

		// 当文件的md5读取完成后,上传文件的状态就变成了等待上传
		this.uploadItem.status = FileStatus.waiting
	}

	private getFileMd5(){
		...
	}
}
  1. 因为要在上传的类中监测上传实例状态的变化,即使上传实例的内存地址相同,我们也不在计算md5的类中去修改上传实例的状态。为此我们需要在计算MD5的类中继承一个自定义的。这样就可以在计算MD5的方法中发送事件
class CalcMd5 extends CustomEvent{

	public isCalcMd5: boolean = false;             // 是否正在计算md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem

		// 此时该上传文件的实例状态变为正在读取MD5
-		this.uploadItem.status = FileStatus.reading
+		this.dispatchEvent('updateFileStatus', uploadItem.uid, FileStatus.reading);
	
	    this.isCalcMd5 = true
		let md5 = await this.getFileMd5();
	    this.isCalcMd5 = false

		// 当文件的md5读取完成后,上传文件的状态就变成了等待上传
-		this.uploadItem.status = FileStatus.waiting
+       this.dispatchEvent('updateFileStatus', uploadItem.uid, FileStatus.waiting);
	}

	private getFileMd5(){
		...
	}
}
  1. 在上传的类中监听上传实例状态的变化的事件
class Uploader{

	public uploadList: UploaderFile[] = [] // 上传的文件列表
	private calcMd5: CalcMd5 = new CalcMd5();

	constructor(){

		// 监听上传实例状态的变化的事件
+		this.calcMd5.on('updateFileStatus', this.updateFileStatus.bind(this))
	
	}

	...

	/**
     * 更新上传实例的状态
     * @param uid 上传实例的唯一标识符
     * @param status 新的状态
     */
+	private updateFileStatus(uid: String, status: FileStatus){
+		let index = this.uploadList.findIndex(item => item.uid === uid)
+		if (index >= 0){
+			this.uploadList[index].status = status
+		}
+	}	
}

  1. 计算完文件的MD5后需要发送更新上传文件实例的MD5的事件了
class CalcMd5 extends CustomEvent{

	public isCalcMd5: boolean = false;             // 是否正在计算md5
	private uploadItem: UploaderFile;               // 上传的对象实例 

	public async addFile(uploadItem: UploaderFile){
		this.uploadItem = uploadItem

		// 此时该上传文件的实例状态变为正在读取MD5
		this.dispatchEvent('updateFileStatus', uploadItem.uid, FileStatus.reading);
	
	    this.isCalcMd5 = true
		let md5 = await this.getFileMd5();
	    this.isCalcMd5 = false

		// 当文件的md5读取完成后,上传文件的状态就变成了等待上传
	    this.dispatchEvent('updateFileStatus', uploadItem.uid, FileStatus.waiting);

		// 当文件的md5读取完成后,发送更新文件MD5的事件
+		this.dispatchEvent('updateFileMd5', uploadItem.uid, md5);
	}

	private getFileMd5(){
		...
	}
}
  1. 接收更新上传文件实例的MD5的事件
class Uploader{

	public uploadList: UploaderFile[] = [] // 上传的文件列表
	private calcMd5: CalcMd5 = new CalcMd5();

	constructor(){

		// 监听上传实例状态的变化的事件
		this.calcMd5.on('updateFileStatus', this.updateFileStatus.bind(this))
		this.calcMd5.on('updateFileMd5', this.updateFileMd5.bind(this))
	
	}

	...

	private updateFileMd5(uid: string, md5: string){
		let index = this.uploadList.findIndex(item => item.uid === uid)
		if (index >= 0){
			 this.uploadList[index].md5 = md5
		}
	}
	
	/**
     * 更新上传实例的状态
     * @param uid 上传实例的唯一标识符
     * @param status 新的状态
     */
	private updateFileStatus(uid: String, status: FileStatus){
		...
	}
}

生成上传的分片

  1. 定义一个上传的分片类
class UploaderChunk{
	public isUploaded:boolean = false; // 是否已经上传

	 constructor(
		 public chunk: Blob,          // 文件的分片
		 public chunkname: string,    // 文件分片的名称
		 public filemd5: string,     // 文件的md5
		 public filesize: number,     // 文件的大小
		 public order: number,        // 分片在文件中的序号
		 public index:number          // 该分片在文件中的缩影
	){}
}
  1. 获取文件分片实例的函数
function getFileChunk(file: File, md5: string, chunkSize: number):UploaderChunk[]{

	// 返回的上传分片实例
	const chunkList: UploaderChunk[] = []

	// 获取文件的分片
	const chunks = UploaderFile.getChunks(file, chunkSize)

	// 对文件的分片进行遍历
	for (let i = 0; i < chunks.length; i++){
		let chunk: Blob = chunks[i]
        const chunkName = `${md5}_${i}`;
	    const fileMd5 = md5;
        const filesize = file.size
        const item = new UploaderChunk(chunk, chunkName, fileMd5, filesize, i)
        chunkList.push(item)
	}

	return chunkList;
}
  1. 在上传的实例中添加该分片
class UploaderFile{

	...
	
	public chunkList: UploaderChunk[] = [];     // 文件分块

	constructor(file:File){
		...
	}
}
  1. 计算完文件的MD5后就可以将文件的分片赋值给上传实例的属性了
class Uploader{

	public uploadList: UploaderFile[] = [] // 上传的文件列表
	private calcMd5: CalcMd5 = new CalcMd5();

	constructor(){

		// 监听上传实例状态的变化的事件
		this.calcMd5.on('updateFileStatus', this.updateFileStatus.bind(this))
		this.calcMd5.on('updateFileMd5', this.updateFileMd5.bind(this))
	
	}

	...

	private updateFileMd5(uid: string, md5: string){
		let index = this.uploadList.findIndex(item => item.uid === uid)
		if (index >= 0){
			 this.uploadList[index].md5 = md5

+			 let file = this.uploadList[index].raw
+			 this.uploadList[index].chunkList = getFileChunk(file, md5, 1024 * 1024 * 5)
		}
	}

	/**
     * 更新上传实例的状态
     * @param uid 上传实例的唯一标识符
     * @param status 新的状态
     */
	private updateFileStatus(uid: String, status: FileStatus){
		...
	}
}
  1. 计算完md5后,开始文件的上传工作,和计算下一个文件的MD5
class Uploader{

	public uploadList: UploaderFile[] = [] // 上传的文件列表
	private calcMd5: CalcMd5 = new CalcMd5();

	constructor(){

		// 监听上传实例状态的变化的事件
		this.calcMd5.on('updateFileStatus', this.updateFileStatus.bind(this))
		this.calcMd5.on('updateFileMd5', this.updateFileMd5.bind(this))
	
	}

	...

	// 计算文件的MD5
	private calcMd5(){
		...
	}

	// 开始文件的上传
	private start(){
	
	}

	private updateFileMd5(uid: string, md5: string){
		let index = this.uploadList.findIndex(item => item.uid === uid)
		if (index >= 0){
			 this.uploadList[index].md5 = md5

			 let file = this.uploadList[index].raw
			 this.uploadList[index].chunkList = getFileChunk(file, md5, 1024 * 1024 * 5)

			// 计算下一个文件的MD5
			this.calcMd5()

			// 开始文件的上传处理
			this.start()
		}
	}

	/**
     * 更新上传实例的状态
     * @param uid 上传实例的唯一标识符
     * @param status 新的状态
     */
	private updateFileStatus(uid: String, status: FileStatus){
		...
	}
}

上传文件的分片

  1. 定义类处理上传文件的类,该类有一个添加文件的公开方法
class UploadHandle extends CustomEvent {
	public uploadItem?: UploaderFile;       // 上传的对象实例 
	public isUploading:boolean = false;     // 是否正在上传

	constructor(
		private upUrl: string,               // 上传的url
		private mergeUrl: string             // 合并的url
	){
		super()
	}

	// 添加文件到上传处理器
	public addFile(uploadItem: UploaderFile): void{
		this.uploadItem = uploadItem
        this.dispatchEvent('updateFileStatus', uploadItem.uid,FileStatus.uploading)
		this.isUploading = true

		
	}
}

  1. 在上传的类使用上传文件分片的方法
class Uploader {
	
+	private uploadHandle:UploadHandle=new UploadHandle('api/up','api/merge')

	...

	private start(){
		 // 判断是否有文件正在上传,多个文件无法同时上传,只能等一个文件上传完成后再上传下一个文件
+		 if(!this.uploadHandle.isUploading){
+			 this.nextUpload()
+		 }
	}

	// 从上传列表中找到正在等待上传的文件,然后将该文件添加到上传处理器中
+	private nextUpload(){
+		const uploadList = this.uploadList.filter(item => item.status === 'waiting')
+       if (uploadList.length > 0) {
+          this.uploadHandle.addFile(uploadList[0])
+       } else {
+            this.uploadHandle.isUploading = false
+       }
+	}

	...
}
  1. 监听上传文件状态变化的方法
class Uploader{

	private uploadHandle:UploadHandle=new UploadHandle('api/up','api/merge');

	constructor(){
		...

		 this.uploadHandle.on('updateFileStatus', this.updateFileStatus.bind(this))
	}

	...

	private updateFileStatus(uid: String, status: FileStatus){
		...
	}
	
	...

}
  1. 再上传的处理中,我们要做三步处理。上传的预处理,上传分片,分片合并
class UploadHandle extends CustomEvent{

	...

	public addFile(uploadItem: UploaderFile): void{
		this.uploadItem = uploadItem

		// 发送将文件上传的状态变为上传中的事件
		this.dispatchEvent('updateFileStatus', uploadItem.uid, FileStatus.uploading)
		
		this.isUploading = true

		let res = await this.preUpload();
		// 通过返回值判断该文件是否上传过,或那些文件块已经上传了
		this.upload()
	}

	private async preUpload(){
		 
	}

	private async upload(){
	 
	}

	private async merge(){
	
	}
}
  1. 上传的预请求
class UploadHandle extends CustomEvent{

	...

	private async preUpload(){
		const url = this.upUrl+'?md5='+this.uploadItem.md5;
		const res = await fetch(url, { method: 'get' })
		return res.json()
	}
}
  1. 判断暂停状态下,处理下一个文件的上传
class UploadHandle extends CustomEvent{

	...

	private async upload(){
		if (this.uploadItem.status === FileStatus.paused){
			this.isUploading = false
			this.dispatchEvent('nextUpload')
		}else{
			// 上传分片
		}
	}
}
  1. 监听下一个上传的请求
class Uploader{

	private uploadHandle:UploadHandle=new UploadHandle('api/up','api/merge');

	constructor(){
		...

		 this.uploadHandle.on('nextUpload', this.nextUpload.bind(this))
	}

	...

	private nextUpload(){
		...
	}
	
	...

}
  1. 上传分片
class UploadHandle extends CustomEvent{

	...

	private async upload(){
		if (this.uploadItem.status === FileStatus.paused){
			this.isUploading = false
			this.dispatchEvent('nextUpload')
		}else{
+			const chunkList = this.uploadItem.chunkList	

			// 当前上传块的索引
+			const index: number = chunkList.findIndex(item => !item.isUploaded)

+			if (index >= 0){
				// 上传分片
+				await this.sendChunk(chunkList[index])

				// 将该分片的状态改为已上传
+				this.uploadItem.chunkList[index].isUploaded = true

				// 上传该文件的下一个分片
+				this.upload()
+			}else{
				
			}
+		}
	}

	... 

	// 上传分片
	private async sendChunk(chunkItem: UploaderChunk){
	
	}
}
  1. 上传分片的请求
class UploadHandle extends CustomEvent{

	...

	private async sendChunk(chunkItem: UploaderChunk){
		const data = new FormData()
        data.append('fileMd5', chunkItem.fileMd5)
        data.append('chunkname', chunkItem.chunkname)
		data.append('order',chunkItem.order)
		data.append('index',chunkItem.index)
        data.append('file', chunkItem.chunk)
        const url = this.upUrl;
        const res = await fetch(url, { method: 'post', body: data })
        return res.json()
	}
	
}
  1. 上传合并
class UploadHandle extends CustomEvent{

	public addFile(uploadItem: UploaderFile): void{
		this.uploadItem = uploadItem

		// 发送将文件上传的状态变为上传中的事件
		this.dispatchEvent('updateFileStatus', uploadItem.uid, FileStatus.uploading)
		
		this.isUploading = true

		let res = await this.preUpload();
		// 通过返回值判断该文件是否上传过,或那些文件块已经上传了
		await this.upload()

		// 该文件上传完成或暂停后,进行下一个文件的上传
		this.isUploading = false
		this.dispatchEvent('nextUpload')
	}

	... 

	private async upload(){
		// 当该上传实例的状态不是暂停时开始上传请求
		if (this.uploadItem.status !== FileStatus.paused){
			const chunkList = this.uploadItem.chunkList	

			// 当前上传块的索引
			const index: number = chunkList.findIndex(item => !item.isUploaded)

			if (index >= 0){
				// 上传分片
				await this.sendChunk(chunkList[index])

				// 将该分片的状态改为已上传
				this.uploadItem.chunkList[index].isUploaded = true

				// 上传该文件的下一个分片
				await this.upload()
			}else{
				await this.merge()
			}
		}
	}

	private async merge(){
	
	}
}
  1. 上传时分片后合并分片请求
class UploadHandle extends CustomEvent{

	...

	private async merge(){
		const uid=this.uploadItem.uid;
		this.dispatchEvent('updateFileStatus', uid, FileStatus.marge)
		
		const filename = this.uploadItem.raw.name
        const md5 = this.uploadItem.md5
        const url = this.mergeUrl+'?md5='+md5+'&filename='+filename
        const response = await fetch(url)
        
        this.dispatchEvent('updateFileStatus', uid, FileStatus.uploaded)
	}
}
  1. 到此,该文件的上传过程走完了,但是我们还需要再上传分片的过程中获取上传的进度和速度
class UploadHandle extends CustomEvent{

	...

	private async upload(){
		if (this.uploadItem.status !== FileStatus.paused){
			// 更新文件上传的进度和速度
+			this.updateProgress()

			...
		}
	}

	// 更新文件上传的进度和速度
	private updateProgress(){
		// 已经上传文件块的大小
		const uploadedSize=this.getUploadSize()

		// 文件的总大小
		const totalSize = this.uploadItem.raw.size

		// 上传的进度
		const progress = parseFloat((uploadedSize / totalSize).toFixed(2))

		// 更新上传的进度,由于对象引用相同,所以上传列表中该文件的上传进度也变化了
		this.uploadItem.progress = progress

		// 计算上次上传块的大小
		const diffSize = (uploadedSize - this.uploadItem._prevUploadedSize)

		// 计算上次上传块的时间
		const diffTime = (Date.now() - this.uploadItem._lastProgressCallback)/1000

		// 更新上传的速度
		this.uploadItem.speed = parseFloat((diffSize / diffTime).toFixed(2)) 

		this.uploadItem._prevUploadedSize = uploadedSize
        this.uploadItem._lastProgressCallback = Date.now()

		// 上传的进度变化后发送事件
        this.dispatchEvent('fileProgress')
	}
}
  1. 监听文件进度变化后的事件
class Uploader{
	
	private uploadHandle:UploadHandle=new UploadHandle('api/up','api/merge');

	constructor(){
		...

		this.uploadHandle.on('fileProgress', this.fileProgress.bind(this))
	}

	...

	private fileProgress(){
		
	}

	...
}

暂停,继续

  1. 暂停
class Uploader{

	...

	public paused(uid: string) {
+		this.updateFileStatus(uid, FileStatus.paused)
    }
	
	...
}
  1. 继续
class Uploader{

	...

	public resume(uid: string) {
		this.updateFileStatus(uid, FileStatus.waiting)
        this.start()
    }
	
	...
}

封装一:事件抛出给用户监听

  1. 将入口的uploader类继承到自定义事件类,用于向使用者发送事件
class Uploader extends CustomEvent{

	...
}
  1. 获取上传实例的列表,用于上传实例状态变化后向向使用者发送列表
class Uploader extends CustomEvent{

	...

	private getFileList(){
		 return this.uploadList.map(item => ({
            uid: item.uid,
            filename: item.raw.name,
            size: item.raw.size,
            mime:item.raw.mime,
            md5: item.md5,
            speed: item.speed,
            status: item.status,
            progress: item.progress,
            readProgress: item.readProgress
        }))
	}
}
  1. 添加文件到上传列表后,发送更新上传列表的事件
class Uploader extends CustomEvent{

	...

	public addFile(file: File){
		const uploadItem = new UploaderFile(file)
        this.uploadList.push(uploadItem)
+		this.dispatchEvent('updateList',this.getFileList());
        this.calcMd5();
	}
	
	...
}
  1. 文件上传实例的状态变化后,发送更新上传列表的事件
class Uploader extends CustomEvent{

	...

	private updateFileStatus(key: String, status: Packtype.FileStatus) {
        let index = this.uploadList.findIndex(item => item.key === key)
        if (index >= 0) {
            this.uploadList[index].status = status
 +          this.dispatchEvent('updateList',this.getFileList());
        }
    }
	
	...
}
  1. 文件上传实例的进度变化后,发送更新上传列表的事件
class Uploader extends CustomEvent{

	...

	private fileProgress(){
		 this.dispatchEvent('updateList',this.getFileList());	
	}
	
	...
}

封装二:参数提取

  1. 上传请求的url、合并请求的url、计算Md5的分片的大小、上传分片的大小需要从外面获取
class Uploader extends CustomEvent{
	...

	constructor(
+		private upUrl: string,               // 上传的url
+		private mergeUrl: string,            // 合并的url
+       private calcMd5ChunkSize:number,     // 计算Md5的分片的大小
+       private uploadChunkSize:number,      // 上传分片的大小
	){
		...
	}
}
  1. 在入口类构造函数中初始化上传处理器的实例
class Uploader extends CustomEvent{

-	private uploadHandle:UploadHandle=new UploadHandle('api/up','api/merge');
+   private uploadHandle:UploadHandle;

	...

	constructor(
		private upUrl: string,               // 上传的url
		private mergeUrl: string,            // 合并的url
	    private calcMd5ChunkSize:number,     // 计算Md5的分片的大小
        private uploadChunkSize:number,      // 上传分片的大小
	){

+       this.uploadHandle = new UploadHandle(this.upUrl,this.mergeUrl)
	}
}
  1. 给计算md5的构造函数中传入计算Md5的分片的大小的属性
class CalcMd5 extends CustomEvent{

	constructor(private calcMd5ChunkSize:number){}

	...

	private getFileMd5(){
		return new Promise((resolve, reject)=>{

			...

			const chunks = getChunks(this.uploadItem?.raw, this.calcMd5ChunkSize);

		})
	}
}
  1. 在入口类构造函数中初始化计算MD5的实例类
class Uploader extends CustomEvent{

	private uploadHandle:UploadHandle;
-   private calcMd5: CalcMd5 = new CalcMd5();
+   private calcMd5: CalcMd5;

	...

	constructor(
		private upUrl: string,               // 上传的url
		private mergeUrl: string,            // 合并的url
	    private calcMd5ChunkSize:number,     // 计算Md5的分片的大小
        private uploadChunkSize:number,      // 上传分片的大小
	){

	    this.uploadHandle = new UploadHandle(this.upUrl,this.mergeUrl)
	    this.calcMd5=new CalcMd5(this.calcMd5ChunkSize);
	}
}
  1. 获取文件分片的实例
class Uploader extends CustomEvent{

	...

	constructor(
		private upUrl: string,               // 上传的url
		private mergeUrl: string,            // 合并的url
	    private calcMd5ChunkSize:number,     // 计算Md5的分片的大小
        private uploadChunkSize:number,      // 上传分片的大小
	){
		...
	}
	
	...

	private updateFileMd5(uid: string, md5: string){
		let index = this.uploadList.findIndex(item => item.uid === uid)
		if (index >= 0){
			 this.uploadList[index].md5 = md5

			 let file = this.uploadList[index].raw
-			 this.uploadList[index].chunkList = getFileChunk(file, md5, 1024 * 1024 * 5)
+			 this.uploadList[index].chunkList = getFileChunk(file, md5, this.uploadChunkSize)
		}
	}
	
	...
}

封装三:Worker封装

现在只要我们在程序的全局构造上传的实例类,但要注意单例,就可以上传的方案了。只需监听上传列表变化的事件就可以获取上传列表的中各个文件实例的状态栏

  1. 由于计算文件的MD5是密集性操作,会使页面卡死,我们可以将上传的方法封装在worker里面
const uploader = new Uploader(
	'api/up',
	'api/merge',
	10*1021*1024,
	10*1024*1024
);

self.onmessage = e => {
    if (e.data.action === 'add') {
        uploader.addFile(e.data.file)
    } else if (e.data.action === 'paused') {
        uploader.paused(e.data.key)
    } else if (e.data.action === 'resume') {
        uploader.resume(e.data.key)
    }
}

uploader.on('updateList',(fileList)=>{
	 self.postMessage({ action: 'updateList', data: this.getFileList() })
})
  1. 使用worker
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });

添加文件到上传列表

worker.postMessage({ action: 'add', file })

暂停某个上传的文件

worker.postMessage({ action: 'paused', key })

继续上传某个文件

worker.postMessage({ action: 'resume', key })

收到上传列表更新的消息

worker.onmessage=e=>{
	if (e.data.action === 'updateList'){
		console.log(e.data.data)
	}
}

优化

  1. 上传预请求的取舍 上传的预请求主要用于判断该文件是否上传过,当然也可以取消。如果取消上传的预请求,则只能在上传第一个分片时判断该文件是否上传过,已经上传了那些分片,这样处理的好处是可以减少一个请求,但弊端是由于上传第一个分片请求由于携带了分片数据,会导致如果该分片已经上传过的无效请求数据。

  2. 合并分片请求的取舍 在上传完某个文件的所有分片之后,需要发送一个合并分片的请求。 这个请求也可以取消 方案一:其处理可以由服务端监听到客户端上传完最后一个分片后去自定合并,合并完后可以由最后一个上传分片的请求返回,也可以由socket.io返回 方案二:服务端在接收分片时根据该分片在文件中的索引插入到文件的指定位置。,但弊端是服务端要单独存放已经上传分片的位置信息

  3. 并发上传分片 先线阶段上传分片只能等前一个分片上传完后才能上传下一个分片,可以并发上传分片,需要指定最大并发请求数量