上传文件详细设计 设计背景
老文件上传为jquery编写的,需要引入jquery文件以及核心js文件
文件上传依赖的库很多年没有更新维护,可能会遇到难以维护的bug
重构基于vue3,使用组件化开发模式,使得代码更加模块化和可维护,避免引入jquery等额外库和依赖
使用TypeScript能够更好地进行代码提示和静态类型检查
增加上传的可定制化能力和可扩展能力
使上传文件模块更加轻量化,保留核心必要逻辑
需求背景
支持多线程进行文件上传,文件上传等候排队
支持分片上传,可通过配置开启关闭
支持文件秒传
定制化文件上传url,以及文件上传完成通知接口url
支持文件上传进度实时回调,文件总进度分进度
支持文件错误重传
支持文件自定义校验
支持上传文件个性化区域定制
兼容旧版文件文件上传服务,可以实现无感替换
整体设计框架 外部交互流程
上传文件系统对外主要就是初始化配置以及文件合规校验:
用户token,用于调起上传接口
可接受的文件类型,array类型有几种类型”Image”, “Video”,”Audio”, “PDF”,”Word”, “Excel”, “PPT”;
根据传入的配置进行类型比对,获取到文件类型后缀,mimeTypes用于读取文件前在文件目录中展示可选择的文件,其他类型的进行隐藏,extensions用于读取文件后对文件类型的二次校验
验证文件大小是否符合
都校验通过后进行记录文件基本信息(文件名,读取进度,上传进度),丢入文件池准备上传
在读文件的方法中获取到当前文件的index 由于读取文件是使用的回调函数的形式,添加额外的参数就需要使用闭包
1 2 3 4 5 6 7 8 9 10 11 12 13 <el-upload :before-upload="beforeUpload(index)" multiple class="flexCenter" :accept="acceptFileType" :http-request="httpRequest" v-else > <div class="upload-placeholder"> <img src="@/assets/uploadImg/icon_import.png" alt="" /> <el-button type="primary">点击选择文件</el-button> </div> </el-upload>
比如我希望在:before-upoad这个回调函数中,传一个自己的参数index
1 2 3 4 5 const beforeUpload = (index ) => { return (file ) => { } }
另一种方式就是
1 :before-upload="(file)=> beforeUpload(file, index)"
这种方式更加简洁
内部交互流程
上传内部主要分为获取文件md5、文件池处理、文件分片处理、文件分片上传
具体设计 文件读取进度回调 组件需要实现实时进度展示,所以需要通过js将目前的文件读取进度告诉调用方
首先需要在upload类中对回调函数进行声明,之后在外部对该回调函数进行重写
1 2 3 4 5 6 public onScanProgressUpdate : Function | undefined = undefined ; public onUploadProgressUpdate : Function | undefined = undefined ; public onUploadComplete : Function | undefined = undefined ;
在文件读取fileReader的回调函数onprogress触发后,会拿到必要的数据,判断组件调用方是否编写了对应的回调函数,如果已经编写了对应函数,则执行回调
1 2 3 4 5 6 7 8 9 10 11 12 fileReader.onprogress = (e ) => { const progress = (e.loaded / file.size ) * 100 ; if ( this .onScanProgressUpdate && Object .prototype .toString .call (this .onScanProgressUpdate ) === "[object Function]" ) { this .onScanProgressUpdate (Math .trunc (progress), fileIndex); } };
文件池处理 整体思路,因为upload 会被调用多次,每次被调用就会向fileList中加入新的文件
此时仅需要判断目前新增的这个文件是否需要立即上传,如果不需要,则排队等待上传对比文件池currnetUploadFileIndex与当前fileList的长度,如果+1 = 长度,则需要立即上传
1 2 3 4 5 6 7 8 9 10 public async addFile (file: File ) { this .fileList .push (file); if (this .currentUploadFileIndex + 1 === this .fileList .length ) { this .upload (); } else { return ; } }
因为外部可能会通过addFile添加很多待上传文件,此时通过currentUploadFileIndex与文件池长度比较,保证每个文件只会被上传一次
只上传当前index的文件,首先获取md5,之后进行分片上传,上传完成之后递归调用上传下一个文件,如果没有文件了就会停止递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async upload ( ) { if (this .fileList [this .currentUploadFileIndex ]) { const md5 = await this .getHashByFile ( this .fileList [this .currentUploadFileIndex ], this .currentUploadFileIndex ); await this .getChunks (this .fileList [this .currentUploadFileIndex ], md5); console .log (`上传完成这是第${this .currentUploadFileIndex + 1 } 个文件` ); this .currentUploadFileIndex ++; this .upload (); } else { return ; } }
控制同时上传的数量 将每一个分片上传的promise放入request队列中,当并发请求数量超过阈值时,将通过promise.race()等待最先完成的请求,每完成上传一次,就从requests数组中将该请求移除,这样就可以保证同时上传的分片数量在期望数量范围
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 39 40 41 async function controlConcurrency (uploadPromise: Promise <void > ) { requests.push (uploadPromise); if (requests.length >= maxConcurrentRequests) { await Promise .race (requests); } } for (let i = 0 ; i < totalChunks; i++) { const start = i * this .chunkSize ; const end = Math .min (start + this .chunkSize , file.size ); const chunk = file.slice (start, end); const uploadPromise = this .uploadChunk (chunk, uploads[i]) .then (() => { const index = requests.indexOf (uploadPromise); if (index !== -1 ) { requests.splice (index, 1 ); } uploadedChunksNumber++; if ( this .onUploadProgressUpdate && Object .prototype .toString .call (this .onUploadProgressUpdate ) === "[object Function]" ) { this .onUploadProgressUpdate ( Math .trunc ((uploadedChunksNumber / totalChunks) * 100 ), this .currentUploadFileIndex ); } }) .catch ((error ) => { console .error ("上传失败:" , error); }); await controlConcurrency (uploadPromise); }
最后通过promise.all()等候所有上传请求完成,进行上传完成后的逻辑