技术栈
前端⽤了React,后端则是EggJs,都⽤了TypeScript编写。断点续传实现原理
断点续传就是在上传⼀个⽂件的时候可以暂停掉上传中的⽂件,然后恢复上传时不需要重新上传整个⽂件。
该功能实现流程是先把上传的⽂件进⾏切割,然后把切割之后的⽂件块发送到服务端,发送完毕之后通知服务端组合⽂件块。其中暂停上传功能就是前端取消掉⽂件块的上传请求,恢复上传则是把未上传的⽂件块重新上传。需要前后端配合完成。前端实现
前端主要分为:切割⽂件、获取⽂件MD5值、上传切割后的⽂件块、合并⽂件、暂停和恢复上传等功能。
切割⽂件:这个功能点在整个断点续传中属于⽐较重要的⼀环,这⾥仔细说明下。我们⽤ajax上传⼀个⼤⽂件⽤的时间会⽐较长,在上传途中如果取消掉请求,那在下⼀次上传时⼜要重新上传整个⽂件。⽽通过把⼤⽂件分解成若⼲个⽂件块去上传,这样在上传中取消请求,已经上传的⽂件块会保存到服务端,下⼀次上传就只需要上传其他没上传成功的⽂件块(不⽤传整个⽂件)。
这⾥把⽂件块放⼊⼀个fileChunkList数组,⽅便后⾯去获取⽂件的MD5值、上传⽂件块等。
// 使⽤HTML5的file.slice对⽂件进⾏切割,file.slice⽅法返回Blob对象let start = 0;
while (start < file.size) {
fileChunkList.push({ file: file.slice(start, start + CHUNK_SIZE) }); start += CHUNK_SIZE;}
获取⽂件MD5值:我们不能通过⽂件名来判断服务端是否存在上传的⽂件,因为⽤户上传的⽂件很可能会有重名的情况。所以应该通过⽂件内容来区分,这样就需要获取⽂件的MD5值。使⽤spark-md5模块获取⽂件的MD5值。模块详情点击这⾥
// 部分代码展⽰
let spark = new SparkMD5.ArrayBuffer();let fileReader = new FileReader();fileReader.onload = e => {
if (e.target && e.target.result) { count++;
spark.append(e.target.result as ArrayBuffer); }
if (count < totalCount) { loadNext(); } else {
resolve(spark.end()); }};
function loadNext() {
fileReader.readAsArrayBuffer(fileChunkList[count].file);}
loadNext();
上传切割后的⽂件块:根据前⾯的fileChunkList数组,使⽤FormData上传⽂件块。
// 部分代码展⽰
Axios.post(uploadChunkPath, formData, {
headers: { 'Content-Type': 'multipart/form-data' }, cancelToken: source.token,}).then(()=>{ // ...})
合并⽂件:就是等所有⽂件块上传成功后发送ajax通知服务端,让服务端把⽂件块进⾏合并。
// 部分代码展⽰
Axios.get(mergeChunkPath, { params: {
fileHash: targetFile,
fileName, },})
暂停功能:把上传⽂件块的请求放到⼀个数组⾥,请求完成的则从数组中删除;点击暂停的时候把数组⾥所有的请求暂停。
/* ⽂件块请求放⼊数组 */
const source = CancelToken.source();// ...
axiosList.push(source);
/* 暂停请求 */
axiosList.forEach((item) => item.cancel('abort'));axiosList.length = 0;
message.error('上传暂停');
恢复上传:去服务端查询已经上传的⽂件块有哪些,然后上传没有上传成功的⽂件块。
// 部分代码展⽰
let uploadedFileInfo = await getFileChunks(this.fileName, this.fileMd5Value);
if (this.handleUploaded(uploadedFileInfo.fileExist) && uploadedFileInfo.chunkList) { this.uploadChunks(this.chunkListInfo, uploadedFileInfo.chunkList, this.fileName);}
后端实现
后端主要的⼯作是针对⽂件的操作,⽐如使⽤fs-extra模块获取⽂件信息、使⽤formidable模块解析上传的⽂件等。
⼤致编写过程:在egg项⽬中的app⽬录⾥⾯找到router.ts⽂件定义路由,定义路由需要传⼊controller⽅法。所以我们接着编写controller⽅法,⽽该⽅法主要对请求参数进⾏处理,调⽤service⽅法处理业务,然后返回结果。主要是router、controller、service三个部分。环境搭建
egg⽂档蛮全的,可以直接参考egg的⽂档。这⾥就简单说下搭建步骤。egg⽂档
⾸先执⾏npm init egg --type=ts安装egg项⽬,然后找到router.ts⽂件定义⼀些路由,⽐如处理上传的接⼝
router.post('api/uploadChunk', controller.file.upload);接着分别在controller⽬录跟service⽬录下创建对应⽂件,⽐如cdapp/controller/ && touch file.ts;最后在对应的⽂件编写具体业务。接⼝编写
主要有三个接⼝,分别是checkChunk、uploadChunk接⼝和mergeChunk接⼝。
checkChunk接⼝:⾸先判断上传的⽂件是否存在,如果存在则告诉前端⽂件已经上传成功。⽂件不存在则再查看存放⽂件块的⽬录是否存在,⽬录存在则把上传成功的⽂件块列表返回给前端。⽬录不存在则把空列表返回给前端。
if (fileInfo.isFileExist) {
checkResponse.fileExist = true;} else {
const fileList = await ctx.service.file.getFileList(fileMd5Val); checkResponse.chunkList = fileList; checkResponse.fileExist = false;}
ctx.body = checkResponse;
uploadChunk接⼝:使⽤formidable模块解析上传的⽂件块,把上传的⽂件块统⼀放到⼀个⽬录,⽤⽂件的MD5值给⽬录命名。
import { IncomingForm } from 'formidable';const form = new IncomingForm();
form.parse(req, async (err, fields, file) => { if (err) return err;
const md5AndFileNo = fields.md5AndFileNo; const fileHash = fields.fileHash;
const chunkFolder = resolve(this.config.uploadsPath, fileHash as string); if (!existsSync(chunkFolder)) { await mkdirs(chunkFolder); }
move(file.chunk.path, resolve(`${chunkFolder}/${md5AndFileNo}`));});
mergeChunk接⼝:通过⽂件MD5值,把对应⽬录⾥⾯的⽂件块⽤createReadStream跟createWriteStream组合成⼀个⽂件。最后在⽂件组合完成之后删除⽂件块⽬录。
const readStream = createReadStream(path);readStream.on('end', () => { unlinkSync(path); resolve();});
readStream.pipe(writeStream);
单元测试
测试⽂件都放在test⽬录⾥,同时必须⽤.test.ts结尾。
编写案例:⾸先创建测试⽂件cd test/app/controller && touch file.test.ts,然后在file.test.ts⾥编写测试代码,最后执⾏npm runtest-local运⾏测试案例。
使⽤app.httpRequest()可以发送HTTP请求,然后传⼊参数,验证返回值是否跟预期相等。
describe('api/checkChunk', () => { // ⽂件不存在的情况
it('should GET / file nonExist', async () => {
const testHash = 'e62d28dd31fc4d1e92a81e7ae5be3cc6'; const result = await app.httpRequest() .get('/api/checkChunk')
.query({ fileName: '归档 2.zip', fileMd5Val: testHash }) .expect(200);
assert.deepEqual(result.body, { hash: testHash, fileExist: false, chunkList: [] }); });});
运⾏
使⽤npm i安装依赖,本地环境启动使⽤npm run dev即可。⽣产环境则先把ts编译成js,执⾏npm run tsc,然后执⾏npm runstart启动服务。代码地址
最后
如果理解了整个断点续传的原理,具体的代码编写就⽐较容易了,可以按照⾃⼰的项⽬需求定制。本⽂提供的代码只是基础实现,仅供⼤家参考。
到此这篇关于React+EggJs实现断点续传的⽰例代码的⽂章就介绍到这了,更多相关React EggJs 断点续传内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!
因篇幅问题不能全部显示,请点此查看更多更全内容